Skip to content

User stories

This document says what the user does with dreamcatcher, why, and what they see, while the ontology says what the words mean, the architecture says where each thing lives in the code and the standard says how good the work has to be. It is an enduring part of the project.

The theory of use is short. One developer runs dreamcatcher on their own machine, under their own GitHub account, against a repository they work in. They hand it work by labelling issues that are assigned to them, and the work is of three kinds: an assignment, where an agent implements the issue and brings it to a pull request; a conversation, where an agent investigates and answers on the issue; and a campaign, where an agent called the lead takes up a parent issue whose sub-issues hold a planned body of work. From then on the developer works as they would with a colleague, reviewing the pull request or replying on the issue. GitHub is where the work happens and where the user acts. The home page and the terminal are a window on what the daemon is doing, with the few controls GitHub has no place for: stop, cancel and retry. The agent hears only the user: a post or a comment from anyone else is never relayed to it as feedback. A campaign's lead works under the user's account, so an assignment's agent hears the lead's feedback as it hears the user's.

Each story is told from the user's seat, in the order things happen, using the ontology's words without defining them and citing no rule and no mechanism. The happy paths come first, grouped as assignments, conversations, campaigns and running dreamcatcher, and each says what the user gained. Then come the user errors, each saying what the user needs to do, and then the expected faults.

Agents should read the stories before judging a finding in the code. A bug is behaviour that contradicts a story, whether a happy path or the handling that a user error or expected fault story promises. A user error is something the user did, and an expected fault is something outside dreamcatcher going wrong; neither cause is itself a bug. Behaviour a story explicitly accepts is not a bug, and better handling than a story promises is an enhancement.

Happy paths

Assignments

The user asks for an issue to be implemented

The user has an issue that says what to build, and would rather an agent built it than spend their own afternoon on it. The change is clear enough to describe, and their time is better spent reviewing it than typing it. They assign the issue to themselves to indicate they have taken responsibility for working on it, and they put an assignment label on it to indicate they want an agent to implement it and select what type of agent. As soon as dreamcatcher has available agent capacity, it sets up a working environment for the agent, including a worktree and a draft pull request on a branch of its own. dreamcatcher then starts a new harness session, and the agent starts work in that branch.

The agent has enough information from the issue description to do a complete implementation, and it works through this, committing and pushing to its branch as it works. The agent works until it has nothing left to do, then marks the pull request ready for review and ends its turn. The user reviews the pull request on GitHub as they would anyone's: comments, inline comments, a request for changes. At the next update, dreamcatcher gives the agent everything the user posted since its last turn. The agent addresses it, pushes, and ends its turn. Anything the user posts while the agent is working waits for the turn after. That exchange repeats as often as the user likes.

While the pull request waits, main may move under it and the checks may go red. The agent deals with both itself, as the moving main story and the failing build story tell.

The user merges when satisfied. GitHub closes the issue, and at the next update the agent takes one last turn to wrap up, which allows it to raise any issues for outstanding work not addressed in the pull request. Until that turn starts, the assignment shows as waiting, awaiting wrap-up. The assignment then ends, and the update after removes its worktree and its local branch, so finished assignments do not fill the disk. Its record stays, so the home page and the assignment's page still show it, without a worktree to resume its session from.

The value to the user is that they need only label an issue, wait for the pull request to be ready, then review it and iterate until happy with the work, then merge, just as they would if delegating a self-contained piece of work to a junior colleague. Everything else is automated by dreamcatcher.

The user asks for several issues to be implemented at once

The user has several issues ready, each self-contained, and wants them all worked on. They assign and label each one. Work begins on them in the order the issues were raised, as many at once as the daemon's capacity allows, and the rest wait for a free slot. Each assignment has its own branch, its own draft pull request and its own agent, so no two interfere, and each pull request becomes ready for review in its own time. Whenever an agent ends its turn, its slot goes to whatever is next in line. The user reviews each pull request as it arrives, in whatever order they like, and the exchanges on different pull requests overlap freely.

With several assignments in flight, GitHub shows each pull request but not the whole. The home page does. Every assignment is a row, and so is every assignment label whose work has not started yet. Each row says whether its work is still to start, working, idle with a pull request ready for review, or ended, and an assignment's page shows what its agent is doing as it works. The user can see at a glance which assignments need them, and read along when they want to check that an agent is on the right track.

The value to the user is that they can delegate as much as they have ready, and keep an eye on all of it from one page.

The user labels issues that depend on each other

The user has two issues where the second builds on the first, so work on it should not start until the first is done. They mark the second as blocked by the first on GitHub, as they would for any pair of issues, then assign and label both. Work starts on the first straight away.

If the user wants to reassure themselves that dreamcatcher is handling the dependencies correctly, they can open the home page or run dreamcatcher status. Either will show the second issue as blocked, naming the issue it waits for.

When the first pull request merges, GitHub closes its issue, and at the next dreamcatcher update the second issue is no longer blocked. Work starts on it, on a branch from main that already carries the first change. The user did nothing between the two beyond reviewing and merging the first.

The value to the user is that they can label a whole chain of issues at once and let the dependencies set the order that agents work on them.

An agent needs user input to complete an assignment

The user wrote the issue as well as they could, but an implementation always turns up something the issue does not settle, and they would rather answer a question than have a guess built. So when the agent, partway through, meets a requirement that reads two ways, a choice the user would want to make, or a test that fails for a reason outside the change, it does not guess. It posts the question as a comment on the pull request, leaves the pull request a draft, and ends its turn.

The user sees the question posted on the pull request and answers it, as they would answer a colleague, and may settle several questions at once or ask one back. At the next update dreamcatcher gives the agent everything the user posted, and the agent carries on in the same session with what it has learned. This may repeat until the agent has what it needs, and the story then continues as the implementation story does, from the point where the agent marks the pull request ready for review.

The value to the user is that they settle a question in one comment at the moment it matters, instead of finding the wrong guess at review.

The user leaves a failing build to the agent

The agent has marked its pull request ready for review, but the checks have gone red: a test it did not run locally, or a lint rule it missed. The user would rather not read the CI log themselves, and they do not have to. The agent is woken to read the failure, fix it and push again. If the checks go red again, it gets one more go. After two attempts without a green build it stops trying and leaves the rest to the user, because two attempts without progress means it needs something it does not have. The row on the home page says the checks are still failing, and now the user reads the failure, and a post telling the agent what it is missing wakes it again.

A draft's checks wake nobody, because the agent has not finished, so only a pull request that is ready for review gets this treatment.

The value to the user is that a red build usually fixes itself before they have even seen it.

The user leaves a pull request that main has moved under to the agent

The agent's pull request is ready for review, and the user has not got to it yet. Meanwhile other work merges to main, and the branch falls behind it, or now conflicts with it. The user does not want to resolve someone else's merge conflict in the agent's branch, and they do not have to. The agent is woken to merge main in and push, without being asked, and the pull request is ready for review again on a current base. If main moves again, so does the agent, once for each move.

One limit is worth knowing. GitHub only reports a branch as behind when the repository requires branches to be up to date before merging. Elsewhere a branch that merely falls behind, without conflicting, wakes nobody until it conflicts or the user posts. That is accepted, because such a branch still merges.

The value to the user is that the pull request they come to review is one that merges cleanly, however long it waited.

The user stops a round and redirects the agent

An assignment has been working for longer than the user expected. They open its page on the dreamcatcher web UI to see what the agent is doing. The feed shows each step the agent takes. When the agent sends subagents to do part of the work, several at once perhaps, the feed leaves out the subagents' own steps, so the agent's own path stays easy to follow. A Claude agent's feed shows each subagent when the agent sends it and its report when it finishes. A Codex agent's feed says when the agent starts to wait on its subagents. Either way, a long quiet spell after that line is the subagents at work, not a hung round.

In this round, the feed shows the agent heading the wrong way: rewriting something the issue never asked for, or chasing a problem that is not the one. Rather than let it finish and review the result, the user stops the round from the page, or with dreamcatcher stop, and it stops within a few seconds. Whatever the agent has pushed so far stays on the branch.

The user goes to the pull request and posts what they want changed, as they would for a colleague. At the next update the agent is given that post, told that the previous round was stopped, and it carries on in the same session, so it remembers what it had done. The agent takes the new direction, pushes, and ends its turn as usual.

The value to the user is that a wrong turn costs minutes rather than a whole round, and the correction goes through the pull request like any other feedback.

The user cancels an assignment to finish its pull request by hand

An assignment has got as far as the agent can take it, and the last stretch is quicker for the user to do by hand than to explain. They cancel it, from its page or with dreamcatcher cancel. Any running round stops, no further round runs, and the pull request, branch and worktree are the user's to work on outside of dreamcatcher, and dreamcatcher never removes them. The assignment's page shows where the worktree is and the command that picks up the agent's session by hand, should they want to carry on the conversation themselves. Once its round has stopped, the assignment's card on the home page, its page and dreamcatcher view show it as cancelled, and while the pull request stays open no new assignment starts for the issue. A cancel that lands as the wrap-up round finishes still keeps the worktree and branch the user's, and a cancel that comes after the wrap-up round changes nothing, so dreamcatcher cancel says that the assignment has already ended. The user finishes the branch and merges it, or closes it, as they would their own.

The value to the user is that taking over is one click and nothing is lost: the agent's commits are theirs to build on.

The user closes a pull request they do not want

The agent's pull request is ready, and having read it the user does not want it: the approach is wrong, or the issue turned out to be a bad idea. Explaining what to change would take longer than starting over, or there is nothing to start over. If they do not want another attempt, they take the assignment label off the issue first. Then they close the pull request without merging. At the next update the agent takes its last turn to wrap up, and the assignment ends. Until that turn starts, the assignment shows as waiting, awaiting wrap-up. Its worktree and local branch are removed, as for a merged pull request, while the branch stays on GitHub with the closed pull request, and the issue stays open. If the user does want a fresh attempt, perhaps after rewriting the issue, they leave the label on or put it back, and a new assignment starts.

The value to the user is that rejecting an agent's work is the same close button as rejecting anyone's.

Conversations

The user asks for an issue to be investigated

The user has an issue they want investigated rather than built: a question about the code, a suspected bug, a design doubt. They assign it to themselves, put a conversation label on it, and write the question as a comment, because the title and body alone ask nothing. At the next update the agent reads the code and answers as a comment on the issue, marked as the agent's own. When a comment needs no reply, the agent posts nothing.

The user replies, and the agent answers again in the same session, so it remembers what it said. A comment the user posts while the agent is still answering waits for its next answer. Each answer is based on main as it stands then, and nothing the agent changed while answering earlier carries over.

What the user does next will depend on what they learned from the conversation. For example, if the conversation verified a bug, then the user might start an assignment for the issue, which leads to a pull request, and ultimately closes the issue as resolved. Or, if the conversation showed a bug report invalid, the user may close the issue as unplanned. Once the issue is closed, the conversation is ended, and its worktree is removed while its record stays.

The value to the user is that they need only label an issue and ask their question as a comment, then read the answer and reply, as they would with a colleague on GitHub who knows the codebase and can investigate. They check out no code, run nothing, and have nothing to clean up afterwards.

The user asks the agent to act on the issue

In the course of a conversation the agent has found that the issue is really three, or that a bug it confirmed deserves an issue of its own. The user would rather not write those up by hand. So they ask, in a comment, and the agent files the sub-issues, or links the related one, and reports what it did in its answer, with every post it makes marked as the agent's own. It will not close the issue or change its labels or assignee, because those would end or pause the conversation, and it will not touch code or branches. What it can do on GitHub is the issue-keeping the user asks for.

The value to the user is that the agent's findings become tracked work in the same breath.

The user stops a round and asks again

The user asked a question, and the answer is taking longer than they expected. They open the conversation's page, and the feed shows the agent has gone off down the wrong path: reading code that has nothing to do with the question, or answering a question they did not ask. Rather than wait for an answer they will not use, the user stops the round, from the page or with dreamcatcher stop, and the stopped round posts nothing.

The user then puts a sharper question in a comment on the issue. At the next update the agent is given it, told that the previous round was stopped, and answers in the same session, so it keeps what it had already learned.

The value to the user is that a misread question costs minutes, not a whole answer.

The user withdraws a question the agent could not answer

The user asked a question, and the round answering it errored, or the daemon stopped before it finished. The conversation shows as waiting, because the next update will give the question again. Before it does, the user decides the question was the wrong one, or no longer matters, and deletes their comment. At the next update nothing is left to answer, so no round starts, and the conversation shows as idle, with its last round still shown as errored or interrupted.

Later the user posts a new question. At the next update the agent answers it in the same session, told that its previous round did not finish.

The value to the user is that deleting a question withdraws it, so the agent never answers it and the conversation does not look as if work is due.

The user pauses a conversation by removing its label

The user has had their question answered, but the issue is going to stay open for a while, perhaps because the fix is for another day. In the meantime they don't want the agent chiming in on every comment, because the discussion that follows is between people. So they take the conversation label off the issue, and that is all it takes. The conversation pauses, and the user can comment on the issue without the agent answering.

Later, something comes up that they want the agent's view on again. They put the label back, and the same conversation picks up where it left off. It still remembers everything it said before, and it is given every comment the user made while it was paused, so they can ask their new question without recapping.

The value to the user is that the agent is on the issue only when wanted, and forgets nothing in between.

Campaigns

The user starts a campaign on a parent issue

The user has planned a body of work as a parent issue, with its work in sub-issues, and wants an agent to take the plan up. The configuration has a campaign route whose prompt tells the lead what to do with the parent and with their comments on it. The dream:lead route that init writes is one, and it carries the sub-issues through their assignments to merge. They assign the parent to themselves and put the campaign label on it. No comment is needed. As soon as dreamcatcher has available agent capacity, it checks out main in a worktree of the campaign's own and starts the lead in a new harness session.

The lead is given the current picture of the parent's sub-issues, with where each one's assignment stands, and the assignment routes that the running daemon offers. It reads the parent and its sub-issues on GitHub, and any comments the user had already left on the parent, and does what its prompt asks, posting on GitHub itself, with every post marked as the agent's own, apart from feedback on a sub-issue's pull request. Its final words stay in the feed, so the parent issue holds only what the lead chose to say there. At later updates the lead is left alone until the user comments on the parent or a sub-issue changes in a way that it needs to hear of. The home page has a Campaigns tab, where the campaign shows as idle with how its round ended and how many of the parent's sub-issues are closed out of the total, and its page shows the feed. The user expands the count to see every sub-issue, each with its assignment's card as the Assignments tab shows it and the row of each request for an assignment on it that has no open work, or the sub-issue's number and title alone when it has neither. The cards keep updating, and the sub-issues stay expanded until the user folds them. Once the campaign ends, it moves to the ended list with its own card alone, and the user goes to GitHub for the history.

Several sub-issues may have open pull requests at once. The user uses a merge queue when GitHub offers one for the repository, so each sub-issue is tested against current main when its turn to merge comes. Otherwise, they leave "Require branches to be up to date before merging" off. If it were on, every merge would leave every other sub-issue behind, waking each assignment's agent to merge main, running its checks again and waking the lead for the new head, though only one pull request can merge next. With it off, a sub-issue that falls behind without conflicting still merges cleanly, and a sub-issue that conflicts still wakes the assignment's agent to bring the branch up to date.

When the user is satisfied, they close the parent, and the campaign ends once any round that is still running finishes. Its worktree is then removed, and its record stays for the ended list. If they reopen the parent with its label still on, a new campaign starts.

The value to the user is that a planned body of work has an agent of its own, started with one label and ended by closing the parent.

The user queues one campaign behind another

The user has planned two bodies of work, each as a parent issue with its sub-issues, and the second should not start until the first is done. They mark the second parent as blocked by the first on GitHub, as they would for any pair of issues, then assign both parents to themselves and put a campaign label on each. The first campaign starts as soon as there is agent capacity. The second waits with no lead, and the home page and dreamcatcher status show its request as blocked, naming the parent it waits for, with its repository when that is another one.

Closing every sub-issue of the first parent does not close that parent, so the second campaign keeps waiting while the user checks the first one's work. When they are satisfied, they close the first parent, and at the next update the second campaign starts, with no further comment or label. A parent with several blockers waits until every one of them is closed. A blocker added to a parent whose campaign has already started changes nothing for that campaign.

The value to the user is that they can line up several campaigns at once and let the dependencies set the order in which the leads take them up.

The user talks to the lead on the parent issue

The user wants to tell the lead something, or ask it how the plan stands. They comment on the parent. At the next update the lead is given the comment, and any others the user has posted since its last round, in the same harness session, so it remembers what it has already done and said. It answers on the parent itself. Its own replies carry the agent's mark, so they never wake it again. While it works the campaign shows as working, with the number of comments and of sub-issues that woke it.

If the lead heads off in the wrong direction, the user stops the round from the campaign's page or with dreamcatcher stop. The lead then waits, rather than starting the same work again. The user comments with fresh direction, and at the next update the lead takes it up in the same session, told that the user stopped its previous round.

The value to the user is that they steer the lead in the same place they read its plan, and the lead never loses the thread.

The lead hears when a sub-issue needs it

A sub-issue's assignment works while the lead waits. When the assignment finishes its turn and no further round of it is due, the next update wakes the lead. So does a sub-issue added to or removed from the parent, closed or reopened, a pull request that changes its head, state or draft flag, checks that finish, a pull request that becomes mergeable, and an assignment that ends or falls into fault. Every change but a sub-issue added or removed waits until the sub-issue's assignment has no round running or due, so the lead is not woken while an assignment is still working, starting work wakes nothing, and neither do checks still running or changes on main. Several changes reach the lead together in one round, beside any comments that the user has posted, and its input names each sub-issue that changed and what changed, with the whole current picture. A change that the lead could not yet be given, because it was working, paused or waiting for an agent slot, reaches it in its next round, and a change that reverted meanwhile is not news. While a round is due for such changes, the campaign shows as waiting with how many sub-issues changed, and the round's trigger names each one with what changed.

The value to the user is that the lead takes up each sub-issue's result as soon as it is ready, and never spends a round on work that is still going on.

The lead gives an assignment's agent feedback

The lead reviews the pull request for a sub-issue's assignment and finds something to correct, or the assignment's agent has asked a question there. The lead comments on the pull request without the agent's mark, and at the next update the assignment's agent is given the comment as it would be given the user's, in its own harness session, and answers on the pull request. The user can comment on the same pull request, and the agent is given both. When the agent has answered and has no round running or due, the lead hears of it and reads the answer on GitHub.

The value to the user is that the lead and each assignment's agent work through the pull request, as the user would, so the user can read the whole exchange and join it.

The user pauses a campaign by removing its label

The user wants the lead to leave the parent alone for a while. They take the campaign label off the parent, or unassign it from themselves, and the campaign pauses. A round that is already running finishes, and no other round starts, not even one to recover a round that did not finish. Comments the user posts on the parent meanwhile wake nothing, but the Campaigns tab keeps counting the closed sub-issues. When they put the label back, the same campaign carries on in its saved harness session, and the lead is given every comment posted while it was paused, and what changed among the sub-issues meanwhile.

The value to the user is that the lead is on the parent only when wanted.

Running dreamcatcher

The user sets up dreamcatcher on a new repository

The user has a repository with a backlog of small, well-described issues, and more of them than they have evenings. They want an agent working through the backlog while they review. They install dreamcatcher as a tool, and make sure the GitHub CLI is signed in as them and Claude Code runs, because dreamcatcher acts as whoever gh is signed in as and runs its agents through the harness. Then, in an ordinary clone of the repository on main, they run dreamcatcher init. It checks what it will need, that it can push, commit and fetch main, writes a default configuration with a route for each of the dream plugin's skills, installs the plugin, and creates the labels on GitHub, printing a line for each step. If a step fails, it says how to fix it, and the user fixes it and runs init again. It ends by telling them to commit and push the configuration, which they do, along with anything an agent will need to build and test the project, because an agent sees only what is on main.

Then they start the daemon from the same checkout with dreamcatcher run, and the home page comes up with nothing on it yet. They assign an issue to themselves, put the dream:smith label on it, and from here the implementation story takes over.

The value to the user is that setup is one command that checks its own prerequisites and says what is missing, and the configuration lives in the repository, so the next person to use it needs only init and run.

The user comes back to see what needs them

The user labelled a handful of issues in the morning and has been in meetings since. Now they want to know what needs them, without opening every pull request to find out. So they open the home page, or run dreamcatcher status if they are already in a terminal. Every assignment, conversation and campaign has a row. So does every dispatch label that no work answers yet, and that row stands for the label until work starts for it. Each kind of work has a section, and within each the rows come in the order that deserves attention: anything in fault first, then what is working, then what is waiting for its turn, then what is idle, then anything the last update could not read, and after all of those the labels where nothing has started yet. Each row says why it is where it is. This pull request is ready for review. That one is still a draft, so the agent has probably asked something. This issue has not started because another pull request already claims it. The pull requests the user needs to read are a click away, and ended work is folded under a collapsed list, so the page shows only what is live. The terminal gives the same rows, with a count of ended work in place of the list.

On the home page, each row keeps what GitHub holds at its top and what dreamcatcher holds at its bottom. The top line gives the issue's number and title, and for an assignment its pull request and where that stands, such as a draft that conflicts with main. A draft's text is subdued. A pull request that is ready for review shows in the theme's brighter text, so it stands out from a draft, and it keeps that text when it conflicts with main or its checks fail. The words "draft" and "ready" stay, so the user can tell them apart without the colour. The assignment's page shows its pull request the same way. A conversation or a campaign has no pull request, so its top line holds the issue alone. Beneath that, a piece of work says what it is doing, with the agent's latest output while a round runs. The bottom line gives the dispatch label, with the harness, model and effort that run the work, and then the work's status and the link to its page. A piece of work that has ended says nothing beneath its top line, and its bottom line gives the state it ended in, in place of its status, so the row says once that it has ended and why. An assignment ends completed or cancelled, and a conversation or a campaign ends closed. The work's page and dreamcatcher view show the same word in place of the status, with nothing beneath it. Every piece of work that has ended shows its title more quietly than live work does, here and within a campaign, so it draws less attention, and its links stay as clear as they are on any other row. A request's row gives its issue at the top and its dispatch label and request status beneath.

The home page always has a tab for each of the three kinds of work, and the terminal a section for each, even when a kind has nothing in it. A kind with nothing active says how to start its work, naming its labels. When the daemon routes no label of a kind, as after an upgrade that brought the kind in, its tab tells the user to add the route to dreamcatcher.toml and restart the daemon to enable the kind, and links to the page of the documentation that shows how. Before any daemon has run, the tabs give no such guidance, because no routes are known yet.

Both show the daemon's last update, and keep showing it after the daemon stops, so the user can still see where things stood.

The value to the user is that they never have to hunt through GitHub to find out what is waiting on them.

The user returns to earlier work at an issue

The user asked about an issue under one conversation label, then switched to another label to try a different recipe, and a second conversation has answered since. Now they want the first discussion back, so they take the second label off and put the first one back. The first conversation picks up where it left off, and the second is paused.

In the terminal, dreamcatcher view GH123 finds two conversations at the issue, so it shows neither. It prints a whole command for each one, with the identifier that selects it, oldest first. The user runs the command for the first conversation, and the view shows that conversation and stays on it. dreamcatcher feed with the same identifier shows what that conversation's agent has said, and nothing from the other one. When a round of it heads the wrong way, dreamcatcher stop with the identifier stops that round and leaves the other conversation alone. If two of its rounds error, dreamcatcher retry with the identifier clears that conversation's fault and no other.

An issue that has had several pieces of agent work, of any kinds, works the same way. Each command given the issue lists a command for each piece, and each piece's identifier selects it. An issue with one piece of agent work needs no identifier.

The value to the user is that the terminal can reach any work on an issue, and never acts on work that the user did not choose.

The user stops and starts the daemon

The user needs to stop the daemon: there is a new version to install, a setting to change, or they are closing the laptop for the night. They do not want to wait for every agent to finish first, and they do not want to lose anything either. So they stop it: they press Ctrl-C, or on macOS and Linux they close its terminal, end it with kill, or shut the machine down. If they started the daemon under nohup, closing its terminal leaves it running, as nohup asks. Every running round ends, and shows as interrupted. When the daemon starts again, each interrupted round's work carries on in the same session, told that the previous round did not finish, and nothing the user posted in between is lost.

The value to the user is that stopping the daemon is always safe, whatever the agents are in the middle of.

The user adds a recipe with a different model

The user has been running the default recipe for a few weeks, and it is not quite right. Perhaps the assignments are burning through their usage allowance on small issues, or perhaps the hard ones come back half done. Either way, they want to try a different model, or a different reasoning effort, without giving up the recipe they have. So they open dreamcatcher.toml and add a second assignment route beside the first: a new label, with a recipe that names the cheaper model and a lower effort, or the stronger model and a higher one. They commit and push the file, so everyone watching the repository gets the same routes, and run dreamcatcher init to create the new label on GitHub. Then they stop the daemon and start it again, which is all it takes for the change to take effect, and nothing in progress is lost.

From then on the label they put on an issue chooses the recipe. Small issues get the light label, hard ones the heavy one, and assignments that were already running keep the recipe they started with. If the new recipe turns out better everywhere, the user edits the original to match, or removes its route, and existing work carries on regardless.

The value to the user is that trying a different model is an edit and a restart, and the choice is made issue by issue with a label.

The user's teammate runs dreamcatcher on the same repository

The user is not the only developer on the repository, and a teammate wants the same help. Nothing more needs setting up: the configuration and the labels are already in the repository, so the teammate clones it, runs dreamcatcher init and then dreamcatcher run on their own machine, and has an instance of their own. Each instance acts only on issues assigned to its own user, so the two never collide, and each user's agents run on their own machine, under their own account, on their own usage allowance. Each agent posts as the user it runs for and hears only that user, so a teammate's review of the user's pull request is read by the user, never by the agent.

An instance does not check who assigned or labelled an issue. Today, anyone who can assign and label issues in the repository can start work on a teammate's instance, under the teammate's account and on their usage allowance. That behaviour is under discussion, not an endorsed way to hand work to a teammate. How the user running the daemon authorizes the work it starts remains an open question, discussed in #594.

The value to the user is that a team shares one configuration and one set of labels, and each user's agents run on their own machine and hear only that user.

The user upgrades dreamcatcher

A new release is out. For a minor or patch release the user stops the daemon, installs the new version, and starts it again, and interrupted rounds carry on as the stop-and-start story says. Before a major release they read the changelog, because a major release may change the state format, and saved work does not carry across formats. In that case the release notes say what to do first: let each assignment reach its wrap-up, or cancel it and finish its pull request by hand, and accept that conversations start afresh, which can mean an agent answering an old comment again if its label is still on. The old state stays on disk untouched, and everything on GitHub stays where it is. Once they have finished with the old state, they can reclaim its disk by hand, as Reclaim disk from an earlier state format describes. Across a change of state format, the user also stops every standalone view, such as dreamcatcher web or a refreshing dreamcatcher status, and starts again from the new installation any they still want. The web page that dreamcatcher run serves stops with its daemon, so it needs no extra step. A view reads only the records of its own state format, so one left running from the old installation may show stale status, and dreamcatcher does not promise to notice.

The value to the user is that a minor upgrade is a restart, and a major one tells them in advance what it will cost.

User errors

Starting a second daemon on the same checkout

The user starts dreamcatcher run in a second terminal, forgetting the first. The second refuses to start and says that another daemon holds the lock, so no two daemons ever work the same checkout. The user needs do nothing but close the second terminal. Running from a linked worktree instead of the main checkout is refused the same way, with a message saying so, and the user needs to run the daemon from the clone's main checkout.

Two labels of one kind on one issue

The user puts two assignment labels on an issue, or two conversation labels, or two campaign labels, perhaps to try both recipes. That is a routing conflict. Until work exists to answer a label, its row stands for that label, so the two labels show as two request rows. Each says there is a conflict and names both labels, and nothing of that kind starts while both are there. The user needs to take one of the two labels off the issue, and the one that stays decides the recipe.

Once an assignment exists, its row stands for that assignment, which keeps the label it started with. While the assignment is open, adding or removing assignment labels on its issue changes nothing. Its label stays as it was, its row says nothing of the conflict, and no further assignment request row appears. If the assignment ends while the issue stays open, the next update reads the issue's labels afresh, and two assignment labels then show as a conflict as before.

A conversation answers only its own label. With two conversation labels, the user may see two request rows, a conversation and a request row, or two conversations, depending on which labels already have one. The conflict stops new rounds. A conversation with a round running finishes that round, and then pauses until the other label comes off. A campaign answers only its own label in the same way.

Labels of different kinds on the same issue are not a conflict, and each can start its own work.

A label without an assignee

The user labels an issue but does not assign it to themselves. Nothing happens, and the issue does not appear on the home page, because dreamcatcher only looks at issues assigned to the user. The user needs to assign the issue to themselves on GitHub, and at the next update it appears and work starts.

A label on an issue someone else is already working on

The user labels an issue that already has an open pull request linked to it, one whose description says it closes the issue. The request row says the issue is claimed by that pull request, and nothing starts while it is open. If the user wants the agent to take over, they need to close the other pull request, and work starts at the next update. If they would rather let it finish, they need do nothing: once it merges, GitHub closes the issue, and the label is moot.

Closing a pull request and leaving the assignment label on the issue

The user closes the agent's pull request without merging, and leaves the assignment label on the issue. The agent wraps up, the assignment ends, and at the next update a new assignment starts, because the label is a standing request. A user who does not want the agent back needs to take the label off before the next update, or cancel the new assignment once it has started.

Removing a label to stop an assignment

The user takes the assignment label off an issue expecting the agent to stop. It does not; the assignment carries on with its pull request whatever happens to the label. To stop it, the user needs to cancel the assignment, from its page or with dreamcatcher cancel.

Committing to the agent's branch

The user sees a small thing they can fix faster than they can describe, and pushes a commit of their own to the agent's branch while the assignment is still open. dreamcatcher does nothing about it. The agent finds the branch has moved when it next pushes, and agents usually work out what happened and build on the user's commit, but nothing told it. If the pull request is ready for review and the checks go red on the user's commit, the agent is woken to fix that as if it were its own. Otherwise the push wakes nobody, so if the user wants the agent to carry on from their commit, they need to say so in a post on the pull request. If they wanted only their fix in, they need do nothing.

Talking to the agent in the wrong place

The user comments on the issue of an assignment, expecting the agent to see it. It does not, because an assignment's channel is its pull request. The user needs to post on the pull request instead. The reverse holds for a conversation and a campaign, whose channel is the issue, so a comment meant for one of them on a pull request needs to go on the issue.

Asking a colleague to give the agent feedback

The user asks a colleague to review the agent's pull request, and the colleague leaves comments on it expecting the agent to address them. Nothing happens. The agent only ever hears the user, because dreamcatcher runs under the user's account and relays nobody else's posts, so the colleague's review is for the user to read. If the user wants the agent to act on it, they need to put the colleague's points in a post of their own, quoting as much or as little as they like. The same goes for a conversation: another person's comment on the issue gets no answer from the agent.

This is deliberate. Only the user's own posts reach the agent as feedback, so a colleague's comment cannot steer work that runs under the user's account. It does not settle who else may start that work, which the teammate story leaves open.

Editing the configuration while the daemon runs

The user changes dreamcatcher.toml and expects the change to take effect. It does not until the daemon next starts, so the user needs to stop the daemon and start it again. If the file is now invalid, the daemon refuses to start and says why, and the user needs to fix the file and start it once more. Meanwhile the home page keeps showing what the last daemon recorded.

Deleting an assignment's worktree

The user deletes the folder under .dreamcatcher/ that an assignment works in. The assignment's next round errors with the reason, and a second failure puts the assignment in fault. The user needs to cancel the assignment and close its pull request. With the label still on the issue, a fresh assignment starts at the next update.

Deleting the daemon lock while the daemon runs

The user deletes .dreamcatcher/daemon.lock while the daemon is running, perhaps to tidy up. The daemon notices at its next update that the lock it holds is gone, prints that it was lost, and stops, ending every running round as a stop of the daemon would, because another daemon could now start on the same checkout. The user needs to start the daemon again, and the interrupted work carries on in its session.

Deleting the whole .dreamcatcher/ folder stops the daemon the same way, but takes every assignment, conversation and worktree with it, so the daemon that starts next knows of no work. Each open pull request still claims its issue, so no second assignment starts for it. The user needs to finish those pull requests by hand, and to expect a conversation whose label is still on to start afresh and answer their earlier comments again.

Editing the daemon's record of the last update

The user edits scheduler.json under .dreamcatcher/, and leaves it so that it no longer reads back. The daemon stops at its next update and prints what is wrong with the file, as it does for any damaged record. The user needs to delete the file and start the daemon again. The next update writes a fresh one, since the file holds only what the last update found.

Adding a sub-issue from another repository

The user adds an issue from another repository as a sub-issue of a campaign's parent. dreamcatcher only sees work in its own repository, so it cannot tell where that sub-issue stands. The campaign's card shows its sub-issues as unknown, and the lead starts no round, not even for a comment on the parent, so it is never given a picture that leaves the sub-issue out or confuses it with a local issue of the same number. A lead that could otherwise take a round shows as unknown, naming the sub-issue that is in another repository. The user needs to remove that sub-issue from the parent, or move the issue into this repository, and the next update gives the lead the whole picture.

Naming work that is not there

The user gives a terminal command an agent work identifier with a typo in it. The command says that no work has that name, and acts on no work at all. It never falls back to other work at the issue. The user needs to give the command the issue instead, and copy the identifier from the commands that it lists. Given a conversation or a campaign, dreamcatcher cancel says that the work is not an assignment, and cancels nothing.

Expected faults

GitHub cannot be reached

GitHub is down, or rate-limited, or the network is. The update's reads fail, nothing starts, and nothing is lost. A failed listing shows among the scheduler failures on the status report while the daemon runs, and a failed read for one piece of work or one request shows on its row. When a campaign's sub-issues, or their assignments, cannot all be read, its card shows its sub-issues as unknown in place of the closed count, and its lead starts no round from the partial picture. A lead that is not working, paused or in fault shows as unknown too, with the failed read; otherwise the lead keeps its status. The next update tries again. Work whose round is already running carries on, and its pushes and posts fail or succeed as GitHub allows.

The harness refuses or breaks

The agent's harness hits a usage limit, is overloaded, or crashes mid-round. The round errors, with the harness's words in the feed. If the harness cannot be started at all, the round errors with the reason, and no harness is left running. The next update gives the work its input again. If that round errors too, the work is in fault, so a problem that two attempts did not clear does not burn more attempts. Nothing runs for it, not even the last turn after a merge, until the user fixes the cause and retries, from the work's page or with dreamcatcher retry. If two pieces of work are in fault at once, a global cooldown begins and the daemon starts nothing for a while, because two faults suggest a shared cause such as an exhausted allowance. When the cooldown ends, the faults it saw are cleared and the daemon tries that work again.

The daemon is killed

The daemon's process is killed outright, by SIGKILL or a crash, so it gets no chance to end its rounds. On macOS and Linux a harness that was running outlives it, keeps working, and may push or post before anyone notices; whatever it did stands. On Windows every way to end the daemon other than Ctrl-C is like this, and the harness dies with the daemon. Either way the round has no ending, so the home page shows its work as waiting. When the daemon next starts, it ends any harness still running, records the round as interrupted, and the work carries on in its session, given its input again.

The daemon knows a harness only by the process identifier that its round recorded, and once the daemon is gone the system may give that identifier to another program. If the machine restarted since the round started, the daemon ends nothing for it, because nothing of the round survived the restart. Without a restart, the identifier may have passed to a program that leads a process group of its own, and the daemon then ends that program and whatever runs in its group. That is accepted: it needs the daemon killed outright and the identifier reused before the daemon next starts.

The machine sleeps

The laptop lid closes with the daemon running. Nothing dies. When the machine wakes, the daemon and its rounds carry on from where they were. If a harness lost its connection while the machine slept, its round errors like any other, and the harness story says what happens next.

The agent does not end its turn

The agent waits on something that never answers, or loops. Nothing in dreamcatcher ends a round for taking too long; the user stops it, from the page or the command line, and posts what they want next. That is accepted, because a long round and a stuck one look the same from outside.

The agent's answer cannot be posted

A conversation round finished but GitHub refused the comment, or the agent returned nothing. The round errors with that reason, and the next update gives it the same comments again.

A local write fails

The disk is full or a folder is unwritable. A write that fails once a round is recorded, while it is prepared or its harness starts, errors the round with the reason, and the next update gives the work its input again. A write that fails anywhere else in an update, such as recording new work, recording that a conversation has ended or writing the daemon's own record of the update, stops the daemon. It prints what failed, ends its running rounds as Ctrl-C does, and leaves the record of the last complete update in place. The user frees the disk or fixes the folder and starts the daemon again, and each interrupted round's work carries on as it does after any stop. No more than that is handled, and that is accepted, because a disk that will not write is the user's to fix, and nothing dreamcatcher holds is the only copy of anything.

An ended piece of work's worktree is not removed

An assignment has completed, or a conversation or campaign has ended with its issue, and Git refuses to remove its worktree or to delete the assignment's local branch, perhaps because the worktree is locked or a file in it is held open. The daemon prints what failed with that update and carries on. The work stays ended, and the worktree or branch stays where it is. The worktree also stays when the daemon stops after an assignment completes and before the update that would remove it. Nothing tries the removal again, and that is accepted, because the leftover takes only disk space, and the user can remove it with Git.

A local record cannot be read

A file under .dreamcatcher/ that an update cannot do without, such as a work record, a round record or the daemon's own record of the last update, is damaged and no longer reads back. At its next update the daemon stops. It prints the file and what is wrong with it, ends its running rounds as Ctrl-C does, and leaves the record of the last complete update in place. The user repairs or restores the file and starts the daemon again, and each interrupted round's work carries on as it does after any stop. A damaged record found while the daemon starts stops it starting, with the same message.

The status report can fail the same way. When a record or feed it needs will not read, dreamcatcher status and a freshly opened page that needs it show only the file and what is wrong with it. A page that is already open keeps what it last showed, as the next story tells.

This is accepted. One damaged file stops every piece of work, not just its own, and nothing sets it aside so that the rest can go on or the rest of the status can show. Such damage should be rare, and a clear report the user acts on is simpler than machinery that works around it. A read from GitHub that fails still belongs to one piece of work, and shows on its row.

A page cannot read what it shows

A record or feed under .dreamcatcher/ will not read back, because something outside dreamcatcher changed it. A home page, assignment page, conversation page or campaign page that is already open keeps the content and status it last showed. A warning on it says that the refresh failed and why. The warning describes only the failed refresh. Until a refresh succeeds, the page cannot say what the work is doing now: the agent may have finished or stopped meanwhile, and the unreadable file may affect the daemon too. The page keeps trying, and once a refresh succeeds the warning goes and any new output shows once. Nothing repairs the record, and that is accepted, because the cause is outside dreamcatcher and the terminal reports the same reason.

A page can also lose its server. The web server that dreamcatcher run starts stops with the daemon, whether the user stopped it or an update could not complete, and a page that is already open then reaches nothing. It keeps the content and status it last showed, and a warning on it says "Refresh failed. Check the terminal running dreamcatcher." The page cannot tell why no answer came, so the warning says nothing of what the daemon or the agents are doing; the terminal says why the daemon stopped. The page keeps trying, and once a refresh succeeds the warning goes.