When project A needs project B: tickets across a portfolio
Inside one project, a to-do is easy. The hard case is when work in one project depends on a change in another — and the person who has to make it is future-me, in a different repo, next week. That boundary needs a ticket, not a mental note.
By Andrew Pyle
Within a single project, tracking work is a solved problem — a list, an issue, a task that rolls up to a goal. The case that quietly breaks is when the work crosses a boundary: I'm deep in project A and I realize it needs a change over in project B before it can be finished. In a team, that's a conversation with another person. Running a portfolio solo, project B is also me — just future-me, next week, in a different repository, having completely forgotten this moment.
That handoff to future-me across a project line is where things fall through. A mental note doesn't survive the context switch. A comment in project A's code never gets seen from project B. The dependency needs to become a thing that exists outside both projects and is visible from the one that has to act on it.
01To-do vs request
The internal to-do versus the crossing request
It's worth being precise about the distinction, because it decides where the work should live. An internal to-do belongs to one project and rolls up to that project's goals — "finish this feature," "fix this bug." It lives with the code it's about. A crossing request is different in kind: it originates in one project but the action has to happen in another. "Project A needs project B to expose this data." "This site needs that shared service to add a field." The defining feature is that the person who files it and the person who resolves it are standing in different repositories, even when they're the same person on different days.
Conflating the two is how work gets lost. If I file a cross-project need as an internal to-do in project A, it'll sit in project A's list where project B never looks, and it'll never get done because the place it's tracked isn't the place the work has to happen. The crossing request needs to be routed to where it will actually be acted on, which is not the project that noticed the need.
In practice I keep these as two genuinely separate mechanisms, not one list with a flag on it. An internal to-do is a task that rolls up to a project's larger goal and lives on that project's own board. A crossing request is a ticket, filed against the project — or the outside party — that has to act, and it lives in a shared place both sides can reach. Picking the wrong container isn't cosmetic: file a cross-project dependency as an internal task and it inherits that task's blind spot, sitting on a board the resolving side never opens.
A dependency between projects is not a to-do in either one. It's a message from the project that needs something to the project that has to provide it — and it has to be visible from the second one.
02Outlive the moment
The ticket has to outlive the moment
The reason a mental note fails here is that the realization and the resolution are separated by a context switch, and context switches are where working memory gets wiped. I notice the dependency while I'm neck-deep in project A's problem. By the time I'm in project B with the ability to fix it, the entire mental frame that produced the realization is gone. So the request has to be durable — written down somewhere that persists past the moment, carrying enough context that future-me can pick it up cold without reconstructing why it mattered.
It also has to be visible from the right place. A ticket that lives only in one machine's terminal or one project's private notes isn't a ticket, it's a diary entry. The whole point is that when I'm working in project B — possibly on a different machine, definitely on a different day — the open requests against project B are right there in front of me. Durable and visible from the resolving side: those two properties are what turn a fleeting realization into work that actually gets done.
03One shared place
One place all the projects can see
The mechanism that makes this work is a shared place, outside any single project, where these crossing requests live — one list that every project can post to and read from. When project A discovers it needs something from project B, it files the request there, against B. When I sit down to work on B, I check that shared list and see what's been asked of it. The requests don't belong to either repo; they belong to the space between them, which is exactly where the dependency lives.
Centralizing it also means the state is the same everywhere I look. Because a portfolio run solo still gets worked across several machines, a request filed from one has to be visible from all of them. What makes that true is that the list isn't a file checked into some repo, where each laptop keeps its own stale copy — it's a single canonical service every machine talks to over the network, so filing from one Mac and resolving from another see identical state. One source of truth for the crossings, so the answer to "what does project B owe?" is the same no matter where I'm standing.
04Close the loop
Filing is the opening move, not the whole move
Writing the request down is only the start of it. A ticket that gets filed and never closed is just a more durable version of the thing it replaced — it still leaves the project that raised the need guessing whether anything ever happened. So the request carries a lifecycle, not just a birth: it's filed against the resolving side, it stays open and visible while the work is outstanding, and then it's resolved with the outcome written onto the ticket itself. The close is the part that matters most, because a closed ticket with a note is the only signal the originating project ever gets back.
Recording the outcome on the ticket is what turns a resolution into something I can see rather than something I have to take on faith. When I'm back in project A weeks later, wondering whether B ever added that field, the answer is a resolved request with a line explaining what shipped — not a hazy sense that I probably handled it. A request with no resolution step is a wish. A request that closes with a recorded outcome is a loop that actually shut, and I can prove it did without reopening the whole investigation.
05Beyond my repos
The boundary doesn't stop at my own repos
The boundary this handles isn't only the one between two of my own repositories. Some crossing requests point outward — at an outside vendor, a data provider, a service I don't control — and the shape is identical: a durable request filed against the party that has to act, visible from where the acting happens, closed when it's done. Whether the resolving side is future-me in another repo or someone else entirely doesn't change the mechanism at all. It only changes who reads the open list, and whether I can nudge it or just have to wait.
There's also a deliberate gate in the middle that I refuse to remove. A filed request doesn't automatically turn into work — it gets triaged first, by me, as a conscious decision that this particular crossing is worth acting on now. Nothing auto-spawns into action straight off a ticket, because a crossing request is a claim on future attention, and claims like that deserve a person deciding when to honor them. It's the same reason I keep a human gate in front of anything that actually commits: the machine can carry the request and hold it indefinitely, but the choice to act on it stays mine and stays deliberate.
06Legible seams
Making the seams between projects legible
There's a deeper reason I bother with this beyond not dropping work. A portfolio of projects has seams — the places where one depends on another — and those seams are invisible by default. Nobody draws them; they live in your head as a vague sense of which things lean on which. Cross-project tickets make the seams legible. The open requests are a live map of exactly where the projects touch and what they owe each other, and that map is worth having on its own, separate from any individual task.
It's the same instinct that runs through how I build everything: make the implicit explicit, especially at boundaries, especially when the thing on the other side is future-me who won't remember. A boundary you can see is a boundary you can manage. The ticket isn't bureaucracy for a solo operation — it's the acknowledgment that even a one-person portfolio has real interfaces between its parts, and interfaces you can't see are interfaces that quietly break.
07
Keep reading
This piece is part of my series on running a fleet of autonomous agents. Start with One operator, a fleet of agents.
Related
writing
I write to my machines in Simplified Technical English
Aircraft maintenance manuals are written in a controlled subset of English designed so they can't be misread. I started using the same discipline for the instructions I give my agents — and it made both the machines and me clearer.
writing
A worktree per agent, so they never step on each other
The moment you run more than one agent on the same repo, they start clobbering each other's files. The fix isn't cleverer coordination — it's giving each one its own copy of the working tree, so parallelism stops being a fight over shared state.
writing
Obsidian is the human interface to my agentic system
Everyone treats Obsidian as a note-taking app. I use it as the async, phone-accessible I/O layer between me and a fleet of AI agents — and here are the actual mechanisms.
writing
Agent skills are the reusable unit I was missing
For a year I treated prompts as disposable. Then I started packaging the ones that worked — and my agents stopped relearning the same job every time.
writing
Dry-run by default
The one rule that lets me run an autonomous system without lying awake about it: nothing writes to the outside world until I say so — and everything it does, it can undo.