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 base comes before the workspace, because the workspace starts from the base.
- 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. - 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.
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 or done.
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.
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.
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 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.
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 operator 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.
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. - 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. - 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 reads the title and the body of the issue. It does not read the comments. 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 an operator 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.
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 operator, 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 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 operator can fix the cause and run the same command again. - 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.
- 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.
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 operator. 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 operator 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 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 operator 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. Such a run tests a tree without the current base. The error tells the user to run
tasker update-branchfirst. - 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, its worktree, its branch and its base revision.
- Read the tip of the task branch and the commits of the branch.
- 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. - Check the answer. Record the review, its findings and the
reviewevent, and move the task.
The command writes one transition event into reviewing, one usage event, one review event with the data { number, revision, findings }, and one transition event out of reviewing. 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 operator 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 operator 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. - 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 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 } and moves the task to accepted. The accepted event takes the place of the transition event for this move.
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.
- 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 branch, the stored base, the base revision and the repo lock. After the check, 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:
tasker mergeruns no check of its own before the push. 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 branch, the stored base and the base revision. 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 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 sourceparent. - The merge base of a stacked task: the replay is the
rebaseform of the move of a stacked task. It replays the commits after the merge base of the task branch and the new tip. No new record is needed. Git skips each old commit of the parent whose patch the new tip already holds. - The strategy of a stacked task: the strategy applies to every task, so it also picks the move of a stacked task.
- The base change after the parent landed: the command switches the base to the base of the parent, also when the head does not move. Without the switch, the task stays stacked, and the guard of
tasker verifyrefuses it each time. - 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. --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 demotion: when the head moved, the old approval or the old verify result no longer describes the tree.
- 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.