This page gives the reasons and the limits of the task commands start, abort, plan, refine, approve, unapprove, execute,
verify, review, dismiss-review, merge, mark-landed, update-branch, finish, retry, resume, approve-credits and accept,
and of the result of an assistant run. The reference files of the user doc SKILL.md, one
for each phase in skills/tasker/references/, show the output.
The commands abort, plan, refine, approve, unapprove, execute, accept, resume, approve-credits, merge, mark-landed, update-branch and finish share one rule. Each of these commands makes all its checks before its first write, so a refused command leaves the task, the files and the events as they were. Accept and Mark landed give an exception for the refs of the clone. A refused approve or unapprove also leaves the plan revisions as they were. Each command that changes a task follows the no-wait rule of the task lock in The locks.
tasker start makes a task for an issue and takes it to workspace_ready.
It has no option to take over a lock of a live process.
The order follows what each step needs and what a failure can leave.
- The issue is read, and the closed check runs, before a task exists. As a result, a bad key, a missing issue or a closed issue leaves no task.
- The blocker step runs before the task exists, so a refusal leaves no new task. On a second run, a refusal leaves the task as the run found it, with no error on record.
- The
base:label and--basewin over the blocker step, because they come earlier in the source order of The resolve. As a result, the step reads no blockers then. - The blocker step writes no learned default. The learned default names a remote branch, because a hint, an answer and
defaultBaseBranchmust name a remote branch (The resolve, and config.md fordefaultBaseBranch). The blocker base is the local branchrefs/heads/<x>of one task. - The note of a missing blocker branch prints at once, so that it also prints when a later source fails. The
baseevent holdsblockeronly for the sourceblocker. The resolve gives the source order. - The base comes before the workspace, because the workspace starts from the base.
- The parent id of a stacked task goes in the same store write as the base. See Stacked task.
- Only a start that makes a new task branch writes the column
synced_base_tipand the keysyncedBaseTipof theworkspaceevent. The value is the tip of the new branch. - Bootstrap runs after the repo lock is free. A bootstrap can run for a long time, and it does not use the clone. If bootstrap holds the repo lock, one slow bootstrap stops the start of every other task in the repo.
Each start and each abort takes the task lock first and the repo lock second. No code takes a task lock while it holds a repo lock. As a result, two commands cannot wait for each other in a circle, so they cannot deadlock.
For each command that changes a task, the task lock does not wait. A second command for the same issue stops at once, because otherwise it repeats the work of the first one. The repo lock covers the steps that change the clone and the question for the base branch. The base step fetches first and asks only after the fetch, so the question falls inside the lock. The repo lock waits, because another task of the same repo usually holds it only for these short steps. The wait is long enough for a first fetch of a large repo.
The rule that each command that changes a task takes the task lock has one home: the locks table.
A person who does not answer the question for the base branch holds the repo lock. Another start of the same repo then waits, and it stops with the lock error at the end of its wait.
A second run continues a task that did not reach workspace_ready. It does not make a second task.
- A failure before the clone steps changes no state and records no error. A busy repo lock is one such failure. The task
stays in
provisioning. - A second run of a failed task moves the task from
failedtoprovisioningafter the checks that run before any write (The order of the steps), and this move is on record. If the repo lock is then busy, the task stays inprovisioningwith no error on record. The next start continues it. - A failure in a clone step or in bootstrap moves the task to
failedwith the error. The next run moves it back and continues. - A task that is past this step is not changed. The command only prints it.
- The stored base stays on a second run. A
--baseoption or abase:label wins and is stored as the new base. A second run with no stored base runs the blocker step. - Each run that writes the base also writes the column
parent_task_idagain, with the rule of Stacked task. - A second run that reuses a task branch or a worktree writes neither the column
synced_base_tipnor the keysyncedBaseTipof theworkspaceevent. - A new base on a second run does not move a task branch that already exists. The stored base can differ from the real start point of the branch.
- An issue that closes after its task exists does not stop a second run. The closed check runs only when no task exists.
Each learned value has its own event: the base, the workspace and the bootstrap result. A value is not part of the next
transition. As a result, a failure in a later step still keeps the earlier values on record. The workspace event holds
syncedBaseTip only when the start made a new task branch.
An agent reads stdout. It holds the fields of the task, then the lines of the tracker writes, so the agent can parse it. The warnings of a failed tracker write go to stderr, and so do the question for the base branch and the notes of the base step.
If issueTracker.updateOnStart is true, the start assigns the issue and marks it in progress. Each write appends one
tracker_update event. The data is { action, result } when the write works, and { action, result: "failed", error }
when it fails. The value of action is assign, in_progress, clear_in_progress, done or abort.
The abort also makes a tracker write. If issueTracker.updateOnStart is true and the repo sets the in-progress label,
it removes the marker with the action clear_in_progress. A failed write is a warning, the same as for the start. If
issueTracker.updateOnStart is true, the repo sets abortStatus and the task has tracker_marked set, the abort of
a Jira task moves the issue to that status with the action abort. If one of these is not true, the abort of a Jira
task runs no tracker step.
The start follows the check-before-write rule in the intro and the rules in The locks. These are the reasons of the tracker writes:
- A failed write is a warning. The reason: the task and the clone are correct, and a person can mark the issue on the tracker.
- The marker check runs before the task exists. The reason: a refused start leaves no task, as The order of the steps says. The check protects against two machines that work on the same issue.
- The writes of start come after
workspace_ready. The reason: a start that fails leaves the issue unmarked. tasker cannot undo the assign, and only an abort removes the marker. - The write of finish comes before the move to
finished. The reason: a second run after a crash runs it again, and the writes are safe to repeat. Writes gives the facts. For Jira, a transition to the status that the issue has now exits with 0 and passes.
A dirty worktree needs --force, because the abort cannot bring the work back. A refused abort also leaves the worktree
and the branch as they were.
The abort of a GitHub task removes the in-progress marker of the issue on the tracker. The reason: the marker protects
an issue against two machines, and after an abort no machine works on the issue.
The tracker writes gives the rule for a failed write. The abort of a Jira task moves the issue to
abortStatus only if this task marked the issue. See The abort write.
A task that is finished or cancelled is an error, because it has nothing to abort.
A task with a run in progress is an error too. The abort reads the run status after the state check and before its first write, so a refused abort changes nothing. The error names the state and the pid of the tasker process. A dead run is not a run in progress. See A dead run.
An abort can stop between two steps, for example after a crash. A second run must finish it. As a result, each step first checks that its target still exists, and a step with no target does nothing.
The compare of the stored worktree path uses the real path. The data root can go through a symlink, and git lists real paths.
The abort removes its own worktree entry. It never prunes, because a prune drops the entries of other tasks too.
The worktree goes before the branch. Git refuses to delete a branch that a worktree entry holds, also when the folder of the entry is gone.
The remove and the delete have one home in src/tasks/. Abort and Finish both call it, and the abort keeps
its discard step before that call.
The abort does not delete the task branch on origin. tasker merge pushes only the base branch, and no command
publishes a task branch.
A worktree or a branch that start made but did not store stays after an abort. The abort works only on what the task stores.
A run is dead when a task is in a spawning state and the tasker process of its run marker is gone.
The run_markers table says how a command finds it. A shared step moves the task to
interrupted and stops the child of the run if it still runs.
The step exists for these reasons:
- The first command that sees a dead run records it. As a result,
tasker statusshows the truth, and an orchestrator can read it. - A resume would start a second assistant in the same worktree. So the step stops the child first.
- The read of the run status and the move share one transaction. As a result, two commands at the same time cannot both write the move.
The move comes before the checks of the command. It is an exception to the rule of the intro that a command makes all its checks before its first write. A command that finds a dead run records it first, even when the command then refuses the task. See The locks for the task lock.
The step takes no task lock. The one transaction of the read and the move is the guard. See Writes from more than one process.
A failed stop of the child is a note on stderr. The task stays in interrupted.
The step has these limits:
- A pid can be reused. A reused pid looks live, so the step can miss a dead run or signal the wrong process group.
- The step finds only a run on this machine. A run on another machine is not found.
One shared step maps the outcome of an assistant run to the next state of the task, or gives the command a quota warning. Each command that runs the assistant (plan, refine, execute and review) must use it. As a result, each command moves the task and prints in the same way, and the rule has one home.
The step records the usage of the run first, for every outcome. Then it acts on the outcome. It moves the task only when the outcome says so below:
- A success with no quota warning causes no move. The command that ran the assistant makes its own checks and its own move.
- A success with a quota warning causes no move. This happens only under
overagePolicy: "ask", when the use of the quota window reachesquotaApprovalThresholdin a window that the user did not approve. The step gives the warning to the command. - A failure and a timeout move the task to
failed. The error goes to the event and to stderr, and the exit code is 1. - A quota stop moves the task to
quota_blocked. The event holds the cause and the reset time when it is known. The command prints the reset time on stdout, or says that it is unknown, and the exit code is 1. - An interrupted run moves the task to
interrupted. This happens when tasker passed SIGINT or SIGTERM to the run, and no quota stop shows in the events. The event holds{ from, to, cause: "signal", signal }. The command prints<phase> interrupted by <signal>; run tasker resume <issue key> to continueon stdout, and the exit code is 1.
After a quota warning, the command pauses the task in awaiting_quota_approval. Plan, refine and review pause before
they record the answer. Execute pauses after it records the result of the turn. The event holds the use and the reset
time when it is known. The command prints both on stdout, or says that the reset time is unknown, and the exit code
is 1.
A failed run also spends tokens, so the usage is recorded for a failure too. The record of usage must be complete.
Plan, refine and review read the tip of the task branch on the remote before this step. If the tip moved or the read fails, this step does not run. The run step of these commands then records the usage itself and fails the task. Execute gives the reasons, and the push guard gives the layers.
The answer of a plan, refine or review run is text, and tasker records nothing before the pause. As a result, the pause drops the answer, and the next run asks again. The commits of an execute turn are already on the branch. As a result, the todo of that turn counts as done, and the next run starts at the next todo.
After an interrupted run, the answer of a plan, refine or review run is dropped. The todo of an execute turn stays
in_progress, because the turn did not end. As a result, tasker resume and the next run continue the session at that
todo.
A quota stop is not a failure. The run can continue after the reset, and the task keeps its session for that.
tasker resume takes the task out of quota_blocked (see Resume). tasker approve-credits takes the task
out of awaiting_quota_approval, or out of quota_blocked after a stop at the overage boundary with a known window
(see Approve-credits). tasker abort leaves either state.
The usage event and the move are two writes, not one. A crash between them leaves the usage event and a task that is still in its spawning state. The run marker of the task stays, so tasker can find that the run died.
tasker show, tasker status and the wrong-state error use one table and one wording, so they name the same commands
for a state. The state check of each command reads the same table. The table and its rules are in
the commands of each state.
tasker plan runs the assistant in plan mode in the worktree of a task in workspace_ready, and records the first plan
revision. The task goes to planning, then to awaiting_plan_review.
The steps run in this order.
- The command also makes its reads before the first write. A failure in this part leaves the task in
workspace_ready. These reads include the tip of the task branch on the remote, which is layer 3 of the push guard. - The command records the session of the plan before it starts the assistant. The session id gives the reason. A task that already has a plan session continues it.
- The command moves the task to
planningand runs the assistant. It appends each event of the run to the transcript of the revision. If the outcome of the run holds a sandbox cleanup, the command then writes onesandbox_cleanupevent with it, before it acts on the outcome. - The command reads the tip again. If the tip moved or the read fails, the command writes the
usageevent and moves the task tofailed. The next steps then do not run. Execute gives the reasons. - The command acts on the outcome as The result of an assistant run says. A plan
that tasker refuses also moves the task to
failed, and the error lists each problem. - The command writes the plan file before it adds the row. Plan files gives the reason.
- The row, its event and the move to
awaiting_plan_reviewshare one transaction. The plan_revisions table gives the rule.
An error after the start of the run also moves the task to failed, with the prefix planning failed:. A failed write
of the plan file moves it to failed too, with its own message. After a crash of tasker, the next command that reads
the task moves it to interrupted. See A dead run.
The plan prompt holds the title, the body and each comment of the issue that the tracker gives. A second tasker plan
works only on a task in workspace_ready.
After a successful plan, the command warns when the plan changes files that another active task of the same repo also
changes. What the plan prompt asks for says that tasker compares
files_changed with the other tasks in flight. The purpose of the warning is to let a user order the landings before
the approve, because the task that lands second can get merge conflicts and redo its work.
The warning reads the files of the newest plan of the other task and the files that its task branch changes. A task that
already executed has files that its plan did not list, so the plan alone is not enough. If the other task is stacked and
its parent landed, the warning reads its task branch against the base of the parent. The reason: tasker finish deletes
the branch of the parent, and tasker update-branch moves the task to the base of the parent. In a chain of landed
parents, the base is the base that the walk of tasker update-branch ends on.
The warning never refuses. Only a person can decide if an overlap is harmless and which task lands first. A failure inside the compare prints one note line on stderr and changes neither the exit code nor a state.
The read takes no repo lock. It changes nothing in the clone, and a wait for the repo lock can hold the end of the command.
tasker refine prints the same warning with the same last line as tasker plan. tasker approve prints the same
warning before its write, with two last lines: tasker approves the plan anyway. and
To take the approval back, run: tasker unapprove <issue key>.
tasker refine continues the plan session of a task in awaiting_plan_review with the feedback of the user, and
records the next plan revision. The task goes to refining, then back to awaiting_plan_review.
tasker refine also runs on a task in reviewed. This is a promotion: the open findings of the latest review go to the
plan session, and the next revision has the source review. The feedback is optional in reviewed, and it is required
in awaiting_plan_review.
tasker refine also runs on a task in failed that has a plan revision. A failure that needs a change of the plan goes
through the same loop as a review finding. The context block gives the origin of the failure (Retry defines
the origin) and the error of the failure. The feedback is required, because it is the instruction for the fix. The
revision has the source refine.
The steps run in this order.
- The command checks the state, the moved-base guard, the feedback, the plan revision, the plan session and the
recorded model before it reads a file. In
reviewed, it also checks that the latest review has an open finding. Infailed, the error for a missing plan revision names the origin andtasker retry. A refused refine changes nothing, as the intro says, so the user can fix the cause and run the same command again. The branch of a landed parent is no base for a new plan (see Stacked task). - The command reads the plan file of the newest revision from disk, so an edit by hand counts. It reads the base revision and the commits of the task branch. It then reads the tip of the task branch on the remote, which is layer 3 of the push guard.
- The command fills the prompt, with the open findings in the context block for a promotion, archives the template and continues the plan session. New and continued sessions gives the rule for the recorded model. The session is already recorded, so refine records no session.
- The command moves the task to
refiningand runs the assistant. The move records the feedback in itstransitionevent, as{ feedback, from, to }, so thattasker resumecan run the same refine again (see Resume). A promotion with no feedback records nofeedbackkey. The run step is the one thattasker planuses, with the prefixrefining failed:for an error. As intasker plan, the run step writes onesandbox_cleanupevent when the outcome of the run holds a sandbox cleanup. The run step then reads the tip again. A moved tip or a failed read fails the task, as intasker plan.
The refine prompt says what the prompt asks for. The new revision is a complete plan.
A question that it leaves out counts as answered by the feedback, and the store computes this. A promotion with no
feedback gives such a question no answer text (the plan_revisions table). The
next revision goes to the next free version, and its transcript goes to the file of that version.
A promotion keeps the findings out of the feedback and leaves them open:
- The findings go in the context block, because the stored feedback holds only the words of the user. The feedback of a
revision is the answer of a question (the
plan_revisionstable), andtasker show-questionsprints that answer. - The promoted findings stay open, because the next review decides if the work fixed them. The review prompt and the findings answer says how the next review reads them.
tasker approve approves the newest plan revision of a task in awaiting_plan_review. The task goes to approved.
Its checks cover the task, the state, the pinned revision and the newest plan revision. The overlap warning comes after the checks and before the write.
The write is one transaction. It sets the approval time of the revision, appends one approved event with the data
{ version, openQuestions, from, to }, and moves the task to approved. The approved event takes the place of the
transition event for this move.
Open questions do not stop an approval. The user decides, and the count in the event and in the output makes the choice visible.
A plan that is older than the last move of the worktree cannot be approved. tasker approve finds the newest
branch_updated event with a how other than none and the newest plan_revision event. If the move is newer, the
command stops. The plan was written against the old tree.
The approval lives on the revision, not on the task. A task has one approved revision for each cycle, and an older approval stays when a newer revision is approved.
tasker unapprove takes back the approval of a task in approved. The task goes back to awaiting_plan_review. Its
checks cover the task, the state and the pinned revision.
The write is one transaction. It clears the approval time of the pinned revision, appends one unapproved event with
the data { version, from, to }, and moves the task to awaiting_plan_review. The unapproved event takes the place
of the transition event for this move. An older approved revision stays approved, because the command clears only the
pinned one.
The command works only in approved. The state alone sets the limit "before execute starts", because tasker execute
moves the task out of approved before it spends anything.
tasker execute carries out the pinned plan revision of a task in approved in the execute session of the assistant.
The task goes to executing, then to executed. The command runs one turn for each todo that is not done, in the
dependency order. Then it runs the queued fix turn of the revision, when the store has one. The command does not run the
verify commands.
The moved-base guard runs right after the check of the state. The branch of a landed parent is no base to build against (see Stacked task).
The steps run in this order.
- The checks cover the state, the pinned revision, the plan file, the order of the todos, the base revision, the branch, the remote tip and the recorded model. The read of the remote tip gives the tip before the run.
- The command fills the prompts and archives only the prompts that the run sends. It records a new session only when the run has a turn.
- The command moves the task to
executingand opens the transcript of the revision for append. - For each turn, the command sets the state of the todo and runs the assistant. After the turn, it records the result and reads the remote tip again.
- The command acts on each outcome as The result of an assistant run says, with the
phase name
executing. An error after the start of the run also moves the task tofailed, with the prefixexecuting failed:. - After the last turn, the command writes the
permission_denialsevent when a turn had a denial. Then it moves the task toexecuted.
The move to executed appends one executed event with the data { version, prompts, from, to }. The executed event
takes the place of the transition event for this move. prompts holds the versions of the prompts that the run sent.
The command also writes todo_started and todo_done with the data { id }, and fix_turn_started and
fix_turn_done with no data. It writes one permission_denials event with { denials }, one sandbox_cleanup event
for each turn that has one, and one usage event for each turn.
A second tasker execute works only on a task in approved.
The reasons:
- The session belongs to the pinned revision. A later run of the same revision continues the session, so the assistant keeps what it learned. A new revision gets a new session.
- The command runs one turn for each todo. As a result, tasker records the state of each todo, and a failed run keeps the todos that are done. A done todo stays done, also when a later turn fails. A done turn already committed its work, and a later failure does not take the commits back. So the next run must not do that todo again.
- A new session gets the prefix with the plan in its first turn. A continued session gets only the turn prompt, because the session already holds the prefix.
- The command reads the remote tip before the run and after each turn. This is layer 3 of
the push guard. A push from the session moves the tip, and the command fails the
task. The command reads the tip after each turn, not only at the end of the run. As a result, the run stops at the
first turn that pushed. No later turn spends tokens or builds on work that already left the machine. A push wins over
the outcome of the turn. A quota stop with a push ends in
failed, because a push breaks a rule of tasker and a quota stop does not. An interrupted turn with a push also ends infailed. Execute prints the line of an interrupted turn only after the push guard. After a push, it prints no resume hint. - A task that is already
failedis not moved again, because a move fromfailedtofailedis illegal. In this case the command only prints the message on stderr. - A turn that reports
blockedor gives no result fails the task. Nobody is present to answer the turn. The user reads the error and decides with the next command. The todo staysin_progress. - All turns share one deadline: the value of
executeTimeoutMin. The command reads the clock once before the first turn, and each turn gets the time that is left. It starts no turn when no time is left.executeTimeoutMinis the limit of one execute run. A limit for each turn multiplies with the count of todos, so a plan with many todos can run for many times the configured limit. - A run with no turn records no execute session and archives no prompt. The rule of
the sessions table says that an id that no conversation holds must not be continued. If
the run recorded one, a later fix turn from
tasker retrycontinues an empty id and fails.
tasker verify runs the verify commands of the repo in the worktree of a task in executed. These reasons explain its
rules:
- The worktree must be clean before the commands. If it is not, the commands test code that the branch does not hold.
- The worktree must be clean after the commands. The worktree goes to a person as ready. A dirty tree makes that claim false.
- A task whose parent landed is refused by the moved-base guard. Such a run tests a tree without the current base. The
error tells the user to run
tasker update-branchfirst (see Stacked task). - Only the exit codes count, because tasker never reads the output of a command. As a result, no output can change a result. phases.md gives the rule.
- A failed run is recorded too.
tasker show-verifyprints the run, and the fix turn gets its log. - The row of a run and the move of the task share one transaction. The bullet in data-root.md gives the reason.
tasker review reviews the committed work of a task in awaiting_work_review. It runs in this order:
- Check the task: its state, the moved-base guard, its worktree, its branch and its base revision.
- Read the tip of the task branch and the commits of the branch. Then read the tip of the task branch on the remote. This read is layer 3 of the push guard.
- Build the text of the earlier reviews, and archive the prompt.
- Prepare a new session, and record it with the next review number.
- Move the task to
reviewing, which writes the run marker. Run the assistant, and write its events to the transcript. - Read the tip on the remote again. If the tip moved or the read fails, write the
usageevent and move the task tofailed. Then skip step 7. - Check the answer. Record the review, its findings and the
reviewevent, and move the task.
The command writes one transition event into reviewing, one sandbox_cleanup event, one usage event, one review
event with the data { number, revision, findings }, and one transition event out of reviewing. The
sandbox_cleanup event comes only when the outcome of the run holds a sandbox cleanup. A failed run writes the usage
event and no review event.
These reasons explain its rules:
- Each review runs in a new session. The review is then an independent pass that the plan conversation does not bias.
- The review runs in plan mode with the limits of the plan role. assistants.md and assistants.md give the rules.
- The findings rest in
reviewed, because the user reads them first. A review with no findings moves the task back toawaiting_work_review. - The review records the tip of the task branch, because the findings describe one tree.
data-root.md gives the reason. The review reads the commits of the task branch, so the
tip of that branch names the tree that the review read. An uncommitted change or a commit that is not on the task
branch does not count, as for
tasker accept. - tasker records the review session although no command continues it yet. assistants.md gives the rule of the session id.
tasker retry runs a failed review again in a new session (see Retry). tasker resume runs a stopped review
again in a new session (see Resume).
tasker review <issue> <feedback> continues the plan session of a task in awaiting_work_review with the feedback of
the user on the committed work. The plan session knows the plan and its decisions. The new revision plans only the work
that is still needed: assistants.md gives that rule. The task goes through refining
to awaiting_plan_review.
The command uses the run step of tasker refine, so the steps of refine apply from the plan revision on. It
runs in this order:
- Take the task lock, and check the state with the row of
reviewin the table of the commands of each state. - Run the moved-base guard. The branch of a landed parent is no base to judge against (see Stacked task).
- Check that the feedback is not empty. The error is
the feedback is empty. - Check the worktree and the branch, and read the tip of the task branch.
- Build the context from the tip and the latest verify run, and run the shared refine step with the source
review-feedbackand the feedback.
The context names no review, and the command reads no review.
tasker dismiss-review dismisses open findings of the latest review of a task in reviewed, with one reason. It runs
in this order:
- Read the task again, and make sure that it is in
reviewed. - Make sure that the reason is not empty.
- Read the latest review, and make sure that the task has one.
- With
--finding, make sure that each named position exists and is open. Without it, take every open position, and make sure that at least one exists. - Write the dismissals and the
review_dismissedevent in one transaction. If no finding is open after that, the move toawaiting_work_reviewis part of the same transaction.
The review_dismissed event has the data { number, positions, reason }. The command writes a transition event only
when the task moves. The event and the move share one transaction.
These links give the rules:
- data-root.md gives the rule for the reason and the rule for an open finding.
- The next review does not report a declined finding again. assistants.md gives the rule.
The command has these limits:
- A dismissal acts only on the findings of the latest review.
tasker retry moves a task in failed back to the state before the step that failed, and runs that step again. The
state that the task left when it failed is the origin. A fixed table gives the target and the next command for each
origin. For the origin provisioning, retry does not move the task. The steps of tasker start move it from failed
to provisioning. For the origin refining, the origin of the refine picks the row. The origin of a refine is the
from of the last move into refining. For the origin merging, the merge origin picks the row. The merge origin is
the from of the last move into merging. For the origins quota_blocked, awaiting_quota_approval and
interrupted, the origin of the pause picks the row of the same table. The rows of the origins refining and merging
do not apply to the origin of a pause. The origin of the pause is the from of the last move into the pause. An origin,
or an origin of the pause, that the table does not hold gets no retry, and the command refuses it.
The move, the delete of the sessions, the todo marks, the fix turn and one retry event share one transaction. The
retry event has the data { origin, target, resumeExecute, from, to } and takes the place of the transition event
for this move. For the origin provisioning, retry writes the retry event alone, with
{ origin, target, resumeExecute }, because no move takes place.
These reasons explain its rules:
- A plan, execute or review run can fail before its first turn. Its recorded session id then names no conversation, so a new run must not continue it. Retry deletes the session of the failed step, and the next run starts a new session. For a review, retry deletes the session of the next review number, so the next review records a new id.
- A verify failure usually belongs to no todo. So
--resume-executemarks every todo done and queues one fix turn that gets the failure. The todos do not run again. - A refine needs feedback, and tasker can refuse a plan again. A promotion from
reviewedand a feedback refine fromawaiting_work_reviewalso need you again, because the findings or the feedback can change. So retry runs nothing for the originrefining. It moves the task back to the state of the origin of the refine and prints a hint, and you decide the next step. - A merge that failed can leave the work on the base of origin. The on-base check of
tasker mergefinds the work on the base, so a second run lands the task with no second push. - After a turn that pushed,
tasker executemoves the task to its pause first, and the push guard then fails the task. So the last move intofailedcomes from the pause, and retry reads the origin of the pause. - The move and the run are two steps with the lock free between them. The next command takes the task lock itself and does not wait, so retry must release the lock first.
- The fix turn gets the tail of the log from tasker, not from the sandbox. The log is in the data root, outside the worktree, and the sandbox gives the shell only the worktree. The bound of 200 lines and 64 KiB keeps a large log out of the prompt.
- The tail comes only from the verify run of this failure. Some verify failures record no run, and then the latest run belongs to an earlier verify. Its log gives the fix turn false data, so the fix turn gets no tail.
tasker resume moves a task in quota_blocked or interrupted back to the state before the run that stopped, and runs
the same command again, except for a task that stopped in updating_branch. For that task, the command first restores
the worktree, and then it runs nothing. The state that the task left when the run stopped is the origin. Resume has a
step for these origins: planning, executing, verifying, reviewing, merging, finishing and updating_branch.
It also has a step for refining, when the origin of the refine is awaiting_plan_review, failed, reviewed or
awaiting_work_review. The origin verifying works only from interrupted. The origins merging, finishing and
updating_branch also work only from interrupted. The Pauses table gives the target and
the next command of these origins, and this page does not copy it. For the origin refining, the origin of the refine
picks the row, as in Retry. For the origin merging, the merge origin picks the row in the same way. For the
origin updating_branch, the origin of the update (the state that the task left when tasker update-branch started)
picks the row, and the task goes back to it. Resume refuses each other origin, a merge origin with no row and an update
origin with no row.
A guard in the shared move refuses a pause that the state machine cannot reach from the origin. The state machine has no
edge from the origin into that pause, so no run stopped that way. As a result, verifying, merging or finishing in
quota_blocked get the no-step error, and so does updating_branch in quota_blocked. tasker approve-credits calls
the same move, so it gets the same guard.
The command makes all its checks before its first write, as the intro says. The move, the delete
of the review session and the events share one transaction. The move writes a transition event { from, to } with the
pause as from. Then it writes a resume event { origin, target }. A resume from updating_branch writes
{ origin, target, head }, with the head that the restore put the worktree back at.
These reasons explain its rules:
- A quota stop and a crash leave a session that holds the work so far. So the resumed plan or execute run continues that
session. A todo that is
donegets no new turn. - A refine needs its feedback, and nobody is present at a resume. So the move into
refiningrecords the feedback, and resume runs the same refine with it. - A review always runs in a new session. The session row of the stopped review names a conversation that the next review must not continue, so resume deletes the session of the next review number.
- A verify run has no session, so the verify runs again from the first command.
- A merge that stopped can leave the work on the base of origin. The on-base check of
tasker mergefinds the work on the base, so a second run lands the task with no second push. - Each cleanup step of
tasker finishskips what is done, so a second run finishes the rest. The target offinishingislanded, also when the task came fromcleanup_failed, becausetasker finishruns fromlandedwith the same steps. - A resume from
updating_branchputs the worktree back at the head from before the update, and then runs nothing. The restore comes before the move. If the restore fails, the command stops, the task stays ininterruptedand nothing is written, sotasker resumecan run again. The restore loses no work:tasker update-branchrefuses a dirty worktree before it starts, and no other command runs while the task is inupdating_branch. The repo lock covers the restore, because the move of the branch changes the clone. - The move and the run are two steps with the lock free between them. The next command takes the task lock itself and does not wait (see The locks), so resume must release the lock first.
The command has these limits:
- It does not hold the task until the reset time of a quota limit. An orchestrator decides when to resume.
- If the assistant never created the session that the next command continues, the resumed run fails.
tasker retrythen starts a new session.
tasker approve-credits moves a task in awaiting_quota_approval, or in quota_blocked after a stop at the overage
boundary with a known window, back to the state before the run that stopped. It records the approved quota window and
runs the same command again. The move is the move of Resume, and this page does not copy it. The command
follows the intro for its checks and The locks for the task lock.
The command writes the resetsAt of the pause record as a credits_approved event. This event shares the transaction
of the move. The run that follows reads it as the approved window.
These reasons explain its rules:
- An approval belongs to one task and one window. The event is on one task and holds the reset time of one window. A later window needs a new approval, and the last approval of the task wins.
- A subscription limit cannot be approved. Paid usage does not continue a task that stopped at the subscription limit.
So the command refuses the stop and points to
tasker resume. - A stop with no window cannot hold an approval. The approval matches a window by its reset time, and a stop with no
reset time has none to match. So the command refuses it and points to
tasker resume.
The command has this limit:
- A change of
overagePolicybetween a pause and its approval is not handled. The approval stores the window of the pause, and the new policy reads that window as it would read any approval.
tasker accept records that a person accepts the work of a task in awaiting_work_review. The task goes to accepted.
It holds the repo lock for the fetch, as the locks table requires. Its checks cover the
task, the state, the moved-base guard, the worktree, the branch, the stored base revision and the fetch of origin with
the read of the base tip. The fetch can still move the refs of origin in the clone, also for a refused accept.
The write is one transaction. It appends one accepted event with the data { revision, baseTip, from, to }, moves the
task to accepted and sets the column accepted_revision to the revision. The accepted event takes the place of the
transition event for this move. The tasks table describes the column.
These reasons explain its rules:
- The base check runs here and not at the landing. As a result, a base that moved while the task waited in review shows before a person lands it.
- The moved-base guard refuses a task whose parent landed. The branch of a landed parent is no base to judge the work against (see Stacked task).
- Accept only records the decision of a person.
tasker mergeis the one step that changes origin. As a result, a refused or an accepted task leaves origin as it was, and the person still decides to land in a separate step.
The base can move after the check of tasker accept. Then tasker merge fails, and nothing moves on origin.
tasker merge lands the work of a task in accepted on the base branch of origin. The task goes through merging to
landed.
Its checks cover the task, the state, the landing guard, the branch, the stored base, the tip of the branch and the repo
lock. The tip check compares the exact ref refs/heads/<branch> of the clone with the accepted revision of the task.
Landing commands gives its errors. After the checks, the
command takes the repo lock and moves the task to merging. Then it fetches origin and reads the tip of the task
branch and the tip of the base. If the base does not contain the work, the command pushes it with
the land operation of the version control provider. The last write is one transaction that
appends one landed event with the data { revision, base, pushed, from, to } and moves the task to landed. The
landed event takes the place of the transition event for this move. If a step fails after the move to merging, the
task goes to failed with merging failed: <reason>. tasker merge does not wait for CI.
These reasons explain its rules:
- The landing guard runs right after the check of the state, before the other checks. A stacked task lands only after each parent in its chain landed and the task moved, so the work lands on the base of the first task (see Stacked task). A stacked task has a local base, so the guard also comes before the error for a local base.
- The compare of the tip with the accepted revision: the review and the accept cover one tree, and the land must cover the same tree. A commit that reaches the branch after the accept has no review.
- The second compare: no lock covers a commit in the worktree. A commit can reach the branch during the fetch. The
command compares again with the revision that it reads after the fetch, and it lands only the accepted revision. A
failed second compare moves the task to
failed, as any step after the move tomergingdoes. tasker mergeruns no check of its own before the push for the base. The land fails before the push when the revision does not build on the tip of the base on origin. The lease refuses the push when the base moves after the read. In both cases nothing moves on origin. version-control.md gives the mechanism.- The on-base check runs first, so a second run after a crash lands with no second push. The check reads the clone, so a missing worktree cannot cause a second push.
- The repo lock covers the fetch and the push, because both change the refs of the clone. The locks table requires it.
- The command takes the repo lock before the move to
merging, so a busy lock changes no state.
tasker mark-landed records that the work of a task in accepted, pushed or in_peer_review already landed on the
base branch outside tasker. The task goes to landed.
Its checks cover the task, the state, the landing guard, the branch and the stored base. Then it takes the repo lock and
fetches origin. It reads the tip of the task branch and the tip of the base with the operation of merge. If the base
does not contain the work, the command stops with an error. The fetch can still move the refs of origin in the clone,
also when the command stops with this error. The last write is one transaction that appends one marked_landed event
with the data { revision, baseTip, from, to } and moves the task to landed. The marked_landed event takes the
place of the transition event for this move.
These reasons explain its rules:
- The landing guard runs right after the check of the state, for the same reason as in Merge. A stacked task
with no own commit has a task branch that equals its base, so without the guard the command would record
landedfor work that did not land. - The check is read-only toward
origin. The work landed outside tasker, so tasker only records it. - The check reads the tip of the task ref in the clone and not the worktree, for the same reason as the on-base check of Merge.
- The fetch holds the repo lock, because the fetch changes the refs of the clone. The locks table requires it.
tasker update-branch brings the task branch current with its copy on origin and brings the worktree of the task up to
date with its base branch. The task goes through updating_branch and then to its origin state or to the target of the
demotion table in the state machine.
These reasons explain its rules:
-
The order of the steps: the command makes all its checks before any write, so a refused command changes nothing. The command takes the repo lock before the move to
updating_branch, so a busy lock changes no state. -
The locks: the task lock does not wait, as The locks says. The repo lock waits, because the fetch and the move of the branch change the clone. The locks table requires it.
-
The
transitionevent intoupdating_branchis{ head, from, to }. Theheadis a commit id of the task branch: the head before any step of the update. It has the same form asbefore.headof thebranch_updatedevent.tasker resumereads it. -
The
baseevent is{ base, revision, source: "update" }. The command writes it only when the head moved. Therevisionis the stored base revision, a ref. The base change after the parent landed writes the event with the sourceparentand the fieldbaseOf: the issue key of the end task of the walk. -
The synced base tip of a stacked task: the replay is the
rebaseform of the move of a stacked task. It replays the commits after the synced base tip of the task onto the new tip. If the head does not build on the synced base tip, or the task has none, it replays the commits after the merge base of the task branch and the new tip, and it writes a note on stderr. The base is a name, because a move reads the current tip of the base. The synced base tip is a commit id, because the replay needs the commit that the task branch builds on. The commits of the parent as they landed passed review and were accepted, and the replay keeps them. On 2026-10-25, each update of #539 tried to replay the old commits of #542 and stopped on a conflict.The replay can also start after a merge base that is newer than the synced base tip, with a note on stderr. After a hand rebase, the head can build on a newer tip. A replay after the synced base tip then takes commits of other tickets.
-
The strategy of a stacked task: the strategy applies to every task, so it also picks the move of a stacked task. A move under
mergeprints a note on stderr for a task that had a parent when the command started. A merge puts the commits of the parent in the history of the task branch, and a later replay underrebasecan still take them. -
The base change after the parent landed: the command walks up the chain of landed parents, from the parent while the next parent has landed. The task where the walk stops is the end task. The command switches the base to the base of the end task, also when the head does not move. The task must not end on the branch of a parent that landed too, because
tasker finishcan delete that branch. The same store write sets the columnparent_task_idto the parent of the end task, or to NULL when the end task has no parent. The command changes the column in no other case (see Stacked task). -
The
branch_updatedevent takes the place of thetransitionevent for the move out ofupdating_branch. Thebaseevent, thebranch_updatedevent, the move and the writes of the demotion table share one transaction. -
The order of the sync step and the base step: the sync step comes first, so that the base step runs on the new local tip.
-
The compare by patch for diverged tips: the command compares the patches of the two sides, because a rebase rewrites each commit. As a result, a branch that someone pushed and then rebased does not count as local work at risk.
-
A merge commit on the remote side always counts as a commit only on origin: it has no one patch, and it can carry a conflict resolution that someone pushed.
-
A merge commit on the local side never counts as local work at risk: it is a merge of the base under
merge, and the base step makes it again. -
The move by itself: when origin has a commit only on origin and no local work is at risk, the command moves the branch to the remote tip without
--take-remote, because the move drops no local work. -
The note case: the tips have diverged, with no commit only on origin and local work at risk. In this case, the command moves nothing, prints a note and exits with 0. This case is not a failure.
originholds no commit that the local side lacks, so no commit oforiginis at risk, and the base step still runs. A refine of a pushed task can replay the pushed commits with no change and add new commits. Each such refine puts the branch in this case. If this case is a failure, each such update stops until the user rewritesoriginby hand, for no gain. The nexttasker pushpublishes the branch again with its lease. On 2026-10-18, the user chose the exit code 0 with the note. -
--take-remoteacts only on a divergence: in each other case, a fast-forward or no move is already right, so the flag has nothing to take and cannot drop local work there. -
The clean worktree for
--take-remote: the take drops the local commits that the remote tip does not hold. The refusal of a dirty worktree beforeupdating_branchmakes sure that the take drops no uncommitted change. -
The undo of the sync move: a failed base step also moves the branch and the worktree back to the local tip from before the sync step, so that a failed update changes nothing.
-
The head of the last move: a rebase rewrites each commit, so the new head does not reach the old head. Only the stored head names the commit that the undo goes back to.
-
The demotion of an undo: the undo moves the head, so the old approval or the old verify result no longer describes the tree.
-
The base of an undo: the base, the base revision and the parent link stay, also after a move that changed the base. The commits of the old parent are in the new base.
-
The synced base tip of an undo: the undo puts the old tip back, because the head builds on that tip. The next replay of a stacked task starts after that tip, so it replays only the commits of the task.
-
The demotion: when the head moved, the old approval or the old verify result no longer describes the tree.
-
The demotion of a head that is not the accepted revision: a task in
accepted, or a task that failed inmerging, goes toexecutedwhen its head is not the accepted revision, also when the command moved nothing. A commit that reaches the branch after the accept moves no base and no copy on origin. Without this demotion,tasker mergerefuses the tip again and again with an error that namestasker update-branch, and that command does not help. This demotion does not changemoved, becausemovedalso decides thebaseevent, and the head did not move. -
The restore rule: the task goes back to its origin state when three facts hold. The head is the head of the start, the worktree is on the task branch, and the worktree is clean. Otherwise the task goes to
failed. -
The missing retry row:
tasker retryhas no row for a task that failed inupdating_branch. The error of a failed restore names the git commands that put the worktree back. After that,tasker update-branchruns fromfailed, ortasker abortends the task.
tasker finish cleans up a task in landed or cleanup_failed: it removes the worktree and the local task branch, and
the task goes through finishing to finished.
The last write appends one transition event with the data { worktree, branch, from, to }. The value of worktree is
removed or already gone. The value of branch is deleted or already gone. If a step fails after the move to
finishing, the task goes to cleanup_failed. Its transition event then has the data { error, from, to }.
If issueTracker.updateOnFinish is true, finish also marks the issue done. The tracker_update event comes before
the last transition event. The tracker writes gives the reason.
These links give the rules that finish follows:
- The command makes its checks before its first write, as the intro says.
- The task lock does not wait, and the repo lock waits. The locks gives both rules.
- Each step first checks that its target still exists, so a second run finishes the steps that are left. Abort gives the rule.
- The worktree goes before the branch, for the reason in Abort.
- The command takes the repo lock before the move to
finishing, for the reason in Merge.
A cleanup that stops goes to cleanup_failed and not to failed. The verify run passed and the work landed, so the
failure says nothing about them.