executor

package
v0.3.32 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 39 Imported by: 0

Documentation

Overview

Package executor runs Claude Code tasks in the background.

Package executor provides task execution with pluggable executor backends.

Index

Constants

View Source
const (
	RemoteInnerPrefix      = "C-a"
	RemoteInnerPrefixHuman = "Ctrl-a"
)

RemoteInnerPrefix is the tmux prefix the attached remote session is given, and the reason it needs one.

Attaching a tmux session inside a tmux pane nests two servers that both listen for C-b. The outer one wins every time, so with the default prefix the inner session cannot be detached, scrolled or copied from — the pane renders but is half-dead. Rather than leave that as folklore, ty sets the inner session's prefix to C-a and says so next to the pane.

View Source
const ConventionalInitScript = "bin/worktree-setup"

ConventionalInitScript is the conventional location for a worktree init script. If no config file exists but this script is present and executable, it will be used.

View Source
const ConventionalTeardownScript = "bin/worktree-teardown"

ConventionalTeardownScript is the conventional location for a worktree teardown script. If no config file exists but this script is present and executable, it will be used.

View Source
const DefaultHandoffTimeout = 90 * time.Second

DefaultHandoffTimeout bounds the wait for an agent to write its handoff. A move is interactive and the agent may be mid-tool-call, so this is generous — but it is bounded, because the most common reason to move a task is that the agent is stuck, and a stuck agent must not block the move that rescues it.

View Source
const DefaultRemoteConnectTimeout = 10 * time.Second

DefaultRemoteConnectTimeout bounds how long ty waits to reach a placed host. A host that cannot be reached in this long is treated as unreachable, and an unreachable host FAILS the task rather than quietly running it here.

View Source
const DefaultSuspendIdleTimeout = 6 * time.Hour

DefaultSuspendIdleTimeout is the default time a blocked task must be idle before being suspended.

View Source
const DefaultTrashRetention = 14 * 24 * time.Hour // 14 days

DefaultTrashRetention is how long a soft-deleted (trashed) task stays recoverable before the daemon sweep hard-deletes it (removes the worktree + row; the Claude transcript is always preserved). Chosen so an accidental delete has a comfortable window to be noticed — the incident that motivated soft-delete went unnoticed for well over a week. Override with the trash_retention setting ("0"/"disabled" = keep trash forever).

View Source
const DefaultWorktreeCleanupMaxAge = 24 * time.Hour // 1 day

DefaultWorktreeCleanupMaxAge is the default time after completion before a done/archived task's worktree is automatically archived and removed to reclaim disk space. Each worktree can hold hundreds of megabytes (node_modules, build artifacts, binary outputs like MP4s), so an aggressive default is important to prevent disk fill.

View Source
const DoneTaskCleanupTimeout = 30 * time.Minute

DoneTaskCleanupTimeout is how long after a task is marked done before its Claude process is killed. This gives users time to review output or retry the task before the process is cleaned up.

View Source
const HandoffPath = ".taskyou/handoff.md"

HandoffPath is where the outgoing agent's handoff lands, relative to the worktree. It is committed with the rest of the work, so it arrives on the target host by the same route everything else does.

View Source
const PlacementChoiceReason = "chosen by hand when the task was created"

PlacementChoiceReason is recorded as the reason for a hand-picked placement. It is shown wherever a placement reason is (ty show, the board, the detail view), so it says who decided rather than what the policy was.

View Source
const SpawnedAtOption = "@ty_spawned_at"

SpawnedAtOption is a window option holding the Unix time EnsureTaskWindow created the window. A just-launched agent pane still reports `sh` as its current command, which is indistinguishable from a dead one by that alone.

View Source
const TmuxDaemonSession = "task-daemon"

TmuxDaemonSession is the default session name that holds all Claude task windows. This is now deprecated - use getDaemonSessionName() for instance-specific names.

View Source
const TmuxOwnerOption = "@ty_owner"

TmuxOwnerOption is the tmux user option naming the machine whose database owns the tasks in a task-daemon session.

It exists because a session's name says nothing about who owns it. A placed task's window lives in a task-daemon session on the REMOTE host, but the task row lives in the database of the machine that placed it. Orphan cleanup asks "is this window's task in my database?", and on the remote host the honest answer for someone else's task is "no" — which used to read as "deleted, kill it" and tore down live agents mid-run.

Variables

View Source
var ClaudeHookEvents = []string{
	"SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse",
	"Notification", "Stop", "StopFailure", "SessionEnd",
}

ClaudeHookEvents are the Claude Code hook events ty installs into a task's worktree. Every one of them is load-bearing for task status, so the set is named once here rather than being implied by whatever setupClaudeHooks happens to write:

  • SessionStart records the Claude session that OWNS the task; everything below is ignored when it comes from any other session (see claimHookSession), because a `claude` the agent runs from Bash inherits WORKTREE_TASK_ID and loads these same hooks
  • UserPromptSubmit returns a blocked task to "processing" on the reply, rather than on the next tool call it may never make, and starts a turn
  • PreToolUse / PostToolUse keep a working task on "processing"
  • Notification marks it "blocked" when Claude wants an answer
  • Stop marks it "blocked" when Claude has finished its turn, and ends that turn. With UserPromptSubmit this is the turn counting behind db.AgentTurn: how a surface that sends a prompt and waits tells the reply to ITS prompt from the end of whatever the agent was already doing
  • StopFailure marks it "blocked" when the turn ended in an error, and records what the provider actually said
  • SessionEnd is the agent's own report that it exited — the one exit signal that is not an inference from a missing tmux window. It writes no status

setupClaudeHooks builds its config from this list, `ty doctor` verifies a live task's settings file against it, and internal/handshake fingerprints it: changing the set changes what the daemon and a client must agree on, so it forces a handshake.Protocol bump.

View Source
var ConfigFileNames = []string{".taskyou.yml", ".taskyou.yaml", "taskyou.yml", "taskyou.yaml"}

ConfigFileNames are the supported configuration file names, in order of precedence.

View Source
var ErrBranchBusy = errors.New("branch is checked out by a running step")

ErrBranchBusy means a step could not start because the branch it needs is checked out by a worktree whose task is still RUNNING.

This is a wait, not a failure. The old behaviour — bubbling the raw git error out of setupWorktree — parked the step 'blocked' with started_at and completed_at one second apart: a step that reads as "ran and finished" on every surface, having never launched an agent. executeTask leaves an ErrBranchBusy task 'queued' instead, so the next daemon tick retries it once the holder is done.

View Source
var ErrExecutorRestartLoop = errors.New("this task's executor was restarted too many times in the last few minutes; not starting another")

ErrExecutorRestartLoop is returned when a task has been launched too many times in a short window. Whatever is relaunching it (two TUIs fighting over the window, an agent that dies on start) will not be fixed by another launch.

View Source
var ErrMemoryPressure = errors.New("executor: deferring spawn, system memory pressure")

ErrMemoryPressure is returned by guardMemoryForSpawn in block mode when the machine is below the free-memory threshold. Callers should treat it as "try this task again shortly", not as a task failure.

View Source
var ErrNoWorktree = errors.New("task has no worktree yet")

ErrNoWorktree means a task in a worktree-isolated project has no worktree yet, so there is nowhere safe to start an agent.

View Source
var ErrPlacedRemotely = errors.New("task is running on another machine")

ErrPlacedRemotely means the task is running on another machine, so this process has no workspace to launch it in and no pane to show.

It is NOT the same failure as ErrNoWorktree, and saying so matters: a remotely-placed task opened in the TUI used to report "task has no worktree yet: refusing to start ... Check your executor configuration", which reads as a broken local setup when the truth is that the task is alive and well on another host.

View Source
var ErrRemoteTerminalEnded = errors.New("no remote executor session running for this task")

ErrRemoteTerminalEnded distinguishes an absent agent from an unreachable host.

View Source
var ErrWorktreeMissing = errors.New("task worktree no longer exists")

ErrWorktreeMissing means the task still records a worktree path, but nothing is there any more — the worktree was reaped (or moved) while the DB row kept pointing at it.

It must be its own failure, and it must be fatal to the start path. tmux's `new-window -c <dir>` does NOT fail on a missing directory: it silently starts the window in $HOME. So a task whose worktree had been reaped would launch an agent in the user's home directory, where the ownership check that follows ("is this pane in the task's worktree?") could never accept the pane it had just created — and the detail view would start another one. That loop burned 178 Claude sessions and $43 in 30 minutes on one task.

Functions

func AgentSender added in v0.3.28

func AgentSender(src WorkSource, daemonSession string, taskID int64) func(string) error

AgentSender returns a send function for a task's live agent, or nil when there is no agent to talk to.

The target is built from what ty recorded when it started the session — daemon session plus the task's window name — rather than by asking tmux which pane is "current", which is the read that has cross-wired two tasks onto one pane before. Sending is best effort by design: every failure path here ends in a synthesised handoff, never in a failed move, because the tasks most worth moving are the ones whose agent has stopped answering.

func AttachmentPrompt added in v0.3.31

func AttachmentPrompt(paths []string) string

AttachmentPrompt uses ordinary file paths understood by every executor.

func CapturePaneContent

func CapturePaneContent(windowTarget string, lines int) string

CapturePaneContent captures the last N lines from a tmux pane. target can be a pane ID (e.g., "%1234") or a window target (e.g., "task-daemon-XXX:2") in which case ".0" is appended to select the first pane. Returns the trimmed content, or empty string if capture fails.

func CapturePaneContentContext added in v0.3.28

func CapturePaneContentContext(ctx context.Context, windowTarget string, lines int) string

CapturePaneContentContext makes one best-effort capture without retrying. Callers doing optional UI enrichment can share a deadline across all panes.

func ChoosePlacement added in v0.3.31

func ChoosePlacement(ctx context.Context, database *db.DB, task *db.Task, target, workDir string) error

ChoosePlacement records a hand-picked placement on a task that has just been created, before anything has run.

target is what the user picked: "" (or "auto") leaves placement to the resolver and writes nothing, "local" pins the task to this machine, and anything else is an SSH destination. workDir is that project's directory on that host; when it is empty the host list is asked for it, so a caller only has to pass along the name the user chose.

This deliberately does NOT go through PlaceTask: that carries work through Git and asks the outgoing agent for a handoff, and a task created five milliseconds ago has neither. The host is not probed either — an unreachable host fails visibly at spawn, the same way a resolver's answer does, and making the new-task form wait on an SSH round trip to save that would be a bad trade.

func ClaudeConfigFilePath

func ClaudeConfigFilePath(dir string) string

ClaudeConfigFilePath returns the path to the claude.json configuration alongside the directory.

func ClaudeSessionExists

func ClaudeSessionExists(sessionID, workDir, configDir string) bool

ClaudeSessionExists checks if a Claude session file exists for the given session ID and workDir.

func ClaudeSettingsPath added in v0.3.31

func ClaudeSettingsPath(workDir string) string

ClaudeSettingsPath is the settings file ty writes hooks into for a task running in workDir. Exported so `ty doctor` can read back what the daemon actually installed instead of guessing the path.

func CleanupClaudeSessions

func CleanupClaudeSessions(worktreePath, configDir string) error

CleanupClaudeSessions removes Claude session files for a given worktree path. Claude stores sessions under CLAUDE_CONFIG_DIR/projects/<escaped-path>/. This should be called when deleting a task to clean up session data.

func DefaultClaudeConfigDir

func DefaultClaudeConfigDir() string

DefaultClaudeConfigDir returns the resolved CLAUDE_CONFIG_DIR taking the environment into account.

func DefaultExecutorName

func DefaultExecutorName() string

DefaultExecutorName returns the fallback executor display name.

func DetectAuthPrompt added in v0.2.40

func DetectAuthPrompt(content string) (string, bool)

DetectAuthPrompt scans captured pane content for signs that the executor's session has been logged out. It returns a human-readable reason and true when a known re-authentication prompt is present.

func DetectBlockingPrompt added in v0.3.28

func DetectBlockingPrompt(content string) (string, bool)

DetectBlockingPrompt reports whether the executor is parked on a question nobody is there to answer, and what it is asking.

A blocking prompt is not a finished agent and must not wait out the idle window to be noticed: the agent has done no work and will do none, so every tick spent confirming that is a tick wasted.

func DetectBusy added in v0.3.28

func DetectBusy(content string) bool

DetectBusy reports whether the screen shows a turn still in flight, and so must not be counted toward the idle threshold however still it looks.

A blocking prompt wins over a busy marker. Claude leaves footer text on screen while a modal is open, so a dialog raised mid-turn can show both at once, and of the two only the dialog is true: nothing is progressing behind it. Answering it is what unsticks the task, so that is what we say.

func DetectStallNotice added in v0.3.28

func DetectStallNotice(content string) (string, bool)

DetectStallNotice reports a visible explanation for why the agent stopped.

func ExecutorBinaries added in v0.3.31

func ExecutorBinaries(slug string) []string

ExecutorBinaries returns the CLI names an executor can run as, or nil for an executor with no binary of its own.

func ExecutorsWithBinaries added in v0.3.31

func ExecutorsWithBinaries() []string

ExecutorsWithBinaries lists, sorted, every executor slug this build knows how to look for on PATH.

func FindClaudeSessionID

func FindClaudeSessionID(workDir string) string

FindClaudeSessionID finds the most recent claude session ID for a workDir using the default config dir. Exported for use by the UI to check for resumable sessions.

func FormatSessionHandoff added in v0.2.32

func FormatSessionHandoff(prevExecutor, capturedContent string) string

FormatSessionHandoff formats captured pane content into a handoff prompt for the new executor.

func GetClaudePIDFromPane

func GetClaudePIDFromPane(paneID string) int

GetClaudePIDFromPane returns the Claude PID for a specific tmux pane. This is used by the UI when it knows the exact pane ID.

func GetTasksWithRunningShellProcess

func GetTasksWithRunningShellProcess() map[int64]bool

GetTasksWithRunningShellProcess returns a map of task IDs that have a running process in their shell pane. A process is considered "running" if the shell pane exists and has a foreground command that differs from the user's default shell.

func GetWorktreeInitScript

func GetWorktreeInitScript(projectDir string) string

GetWorktreeInitScript returns the path to the worktree init script for a project. It checks: 1. The init_script configured in .taskyou.yml 2. The conventional bin/worktree-setup script if it exists and is executable Returns empty string if no init script is configured or found.

func GetWorktreeTeardownScript

func GetWorktreeTeardownScript(projectDir string) string

GetWorktreeTeardownScript returns the path to the worktree teardown script for a project. It checks: 1. The teardown_script configured in .taskyou.yml 2. The conventional bin/worktree-teardown script if it exists and is executable Returns empty string if no teardown script is configured or found.

func HandoffRequest added in v0.3.28

func HandoffRequest(destination string) string

HandoffRequest is the prompt sent to a live agent before its task moves.

func HasLocalState added in v0.3.28

func HasLocalState(task *db.Task) (string, bool)

HasLocalState reports whether a task has already done work on THIS machine, and names that work as a noun phrase ("the first attempt's worktree") for callers to compose into their own sentence — "Staying here: X is on this machine", "moving it would strand X on this machine".

Two things count, and both are unfakeable evidence of a local first attempt:

  • A worktree directory that exists on this disk. Its branch holds the commits, and no other host can see them.
  • A recorded executor session. A Claude session file lives only on the machine that created it, so resuming one anywhere else silently starts a new, empty conversation.

The worktree is checked with a stat rather than trusted from the row: a task whose worktree was cleaned up has nothing left here to strand, and pinning it local for that would be pinning it to a path that no longer exists.

func HasRunningProcessInTaskUI

func HasRunningProcessInTaskUI() bool

HasRunningProcessInTaskUI checks if the task-ui session has a running process in the shell pane (pane index 2). This is used to detect running processes for the currently viewed task, whose panes are joined to task-ui rather than being in the daemon.

func HiddenShellWindowName added in v0.3.31

func HiddenShellWindowName(taskID int64) string

HiddenShellWindowName is the daemon-session window a task's hidden shell is parked in.

func IsManagedWorktree added in v0.3.2

func IsManagedWorktree(path string) bool

IsManagedWorktree reports whether path is inside a taskyou-managed worktree (under a .task-worktrees/ directory). Mirrors the check used elsewhere in the executor for task.WorktreePath.

func KillAllWindowsByNameAllSessions added in v0.2.32

func KillAllWindowsByNameAllSessions(windowName string)

KillAllWindowsByNameAllSessions kills ALL windows with a given name across all daemon sessions. Also kills any -shell variant windows.

func KillTaskWindows added in v0.3.30

func KillTaskWindows(ctx context.Context, taskID int64) bool

KillTaskWindows kills a task's tmux window in every daemon session that holds one, taking the agent process with it. Returns true if any window was killed.

Package-level so `ty sessions suspend` can reuse it without constructing an Executor: the CLI and the daemon sweep must tear a session down the same way.

func LocalOwnerTag added in v0.3.28

func LocalOwnerTag() string

LocalOwnerTag identifies this machine as the owner of the sessions it creates.

func ManagedWorktreeProjectDir added in v0.3.2

func ManagedWorktreeProjectDir(worktreePath string) string

ManagedWorktreeProjectDir returns the project root for a managed worktree path (the part before the .task-worktrees/ segment), or "" if the path is not a managed worktree.

func NormalizeClaudeConfigEnv added in v0.3.13

func NormalizeClaudeConfigEnv() (removed string)

NormalizeClaudeConfigEnv removes an inherited CLAUDE_CONFIG_DIR from this process's environment so Claude config-dir resolution is driven solely by per-project configuration (projects.claude_config_dir) with a fixed ~/.claude default — identical across the daemon, the TUI, the MCP server, and every CLI invocation regardless of the shell each was launched from. It returns the value that was removed (or "" if none) so callers can log it. Every ty entrypoint must call this before resolving any config dir or spawning Claude.

Why this exists: a ty process inherits whatever CLAUDE_CONFIG_DIR its launching shell had. When the daemon is started from inside a Claude Code session running in a project with a custom config dir (e.g. CLAUDE_CONFIG_DIR=~/.claude-ik), the daemon resolves the "default" config dir to ~/.claude-ik, writes task sessions and worktree .envrc files there, and spawns Claude with that inherited dir. A separate ty process launched from a clean shell (e.g. the TUI) resolves the same default-config project to ~/.claude. The two disagree: the TUI's session-resume existence check looks in ~/.claude, can't find the session that actually lives in ~/.claude-ik, declares it gone, clears the stored session ID, and starts a fresh Claude session — silently destroying an in-progress conversation. Projects that need a non-default config dir must set it explicitly via projects.claude_config_dir, which is honored independently of the ambient environment.

func PlacementChoices added in v0.3.31

func PlacementChoices(ctx context.Context, database *db.DB, project, executorName string) []hooks.Host

PlacementChoices returns the hosts a user may choose between for a new task in this project, newest question first: is anything offering a choice at all?

An empty slice means no — no placement plugin installed, no host serving this project, or a handler that could not answer in time — and every surface then shows no host picker, which is the behaviour every user without a fleet has always had.

func PurgeStaleClaudeProjectConfigs

func PurgeStaleClaudeProjectConfigs(configPath string) (int, error)

PurgeStaleClaudeProjectConfigs removes entries from claude.json for paths that no longer exist. Returns the number of entries removed and any error encountered.

func RemoteAttachNotice added in v0.3.28

func RemoteAttachNotice(loc RemoteTaskLocation) string

RemoteAttachNotice is the badge shown beside an attached remote pane. It names the host, so nobody mistakes the pane for a local agent, and the branch, so it is clear which worktree over there is being watched.

It is a badge and not a sentence on purpose: it shares one right-aligned header line with the status, project, type and PR badges, and prose there wraps and strands its own tail on a line of its own. The inner tmux prefix — the one thing about a nested session a user cannot guess — is documented where the keys actually go instead: on the remote pane's border title and in the tmux status bar (see attachRemotePane).

func RemoteAttachScript added in v0.3.28

func RemoteAttachScript(task *db.Task, loc RemoteTaskLocation) string

RemoteAttachScript renders the LOCAL shell line a pane runs to show a remotely placed task's live session, and to explain itself when that session ends.

The pane must never blank or hang. ssh returns whenever the session ends or the link drops, and what follows it — the reason, and the attach command to use by hand — is the whole point of wrapping it in a shell instead of running ssh as the pane's process directly.

func RemoteEndedMessage added in v0.3.28

func RemoteEndedMessage(loc RemoteTaskLocation) string

RemoteEndedMessage is what a local surface says when the host answered and the task's window is not there. It keeps the attach command visible: the window may be gone, but the worktree, the branch and the host are still where the work is.

func RemoteShellAttachScript added in v0.3.28

func RemoteShellAttachScript(task *db.Task, loc RemoteTaskLocation) string

RemoteShellAttachScript attaches a view to the persistent shell ensured by InspectRemoteTerminal. Closing the view leaves the remote shell running.

func RemoteTaskMessage added in v0.3.28

func RemoteTaskMessage(loc RemoteTaskLocation) string

RemoteTaskMessage is the one-line explanation a local surface shows instead of panes for a remotely placed task. It never blames the local configuration, because nothing local is wrong.

func RemoteUnreachableMessage added in v0.3.28

func RemoteUnreachableMessage(loc RemoteTaskLocation) string

RemoteUnreachableMessage is what a local surface says when it could not reach the host at all — which is NOT the same as the task being over, and must not read as if it were.

func RemoveClaudeProjectConfig

func RemoveClaudeProjectConfig(configPath, projectPath string) error

RemoveClaudeProjectConfig removes a project entry from claude.json. This should be called when deleting a worktree to clean up stale config entries.

func RenameClaudeSession

func RenameClaudeSession(workDir, newName, configDir string) error

RenameClaudeSession renames the Claude session for a given workDir to the new name. It uses Claude's /rename slash command via print mode. This is useful when a task title changes and we want the Claude session to reflect it.

func RequestHandoff added in v0.3.28

func RequestHandoff(ctx context.Context, src WorkSource, task HandoffTask, destination string,
	send func(string) error, timeout time.Duration) (string, bool)

RequestHandoff asks a live agent for a handoff and waits for it to appear.

send delivers the prompt to the agent; a nil send (or one that fails) means there is no agent to ask, which is not an error — a task whose agent has died is exactly the task you most want to move. In every failing case this returns a synthesised handoff instead, so the target host always gets something.

func ResolveClaudeConfigDir

func ResolveClaudeConfigDir(custom string) string

ResolveClaudeConfigDir resolves a custom CLAUDE_CONFIG_DIR override. If custom is empty, the default directory (respecting CLAUDE_CONFIG_DIR env) is returned.

func SpawnLockDir added in v0.3.31

func SpawnLockDir() string

SpawnLockDir is executorSpawnLockDir for callers outside the package that must take the same per-task spawn lock (the detail view, before it kills a window it believes is dead).

func StageAttachments added in v0.3.31

func StageAttachments(ctx context.Context, database *db.DB, taskID int64, workDir string, remote *RemoteRunner, ids []int64) ([]string, error)

StageAttachments copies selected attachments into the execution host's worktree. nil IDs means all task attachments (launch/retry); explicit IDs are a reply's selection. Paths belong to that host. Files survive the agent turn for resume.

func SupportsRemoteExecutor added in v0.3.28

func SupportsRemoteExecutor(name string) bool

SupportsRemoteExecutor reports whether a remote launch adapter is available.

func SyntheticHandoff added in v0.3.28

func SyntheticHandoff(task HandoffTask, destination, because string) string

SyntheticHandoff is the handoff written when the agent could not supply one.

It says plainly that it is not the agent's account, and why. The failure mode worth avoiding is a confident-looking summary that the next agent trusts: ty does not know what the previous session was thinking, and pretending otherwise would send the new agent down a path nobody actually took.

func TmuxSessionName

func TmuxSessionName(taskID int64) string

TmuxSessionName returns the full tmux target for a task (session:window).

func TmuxWindowName

func TmuxWindowName(taskID int64) string

TmuxWindowName returns the window name for a task.

func WithRunner added in v0.3.28

func WithRunner(ctx context.Context, r Runner) context.Context

WithRunner returns a context whose commands are built through r.

This is how a per-task placement decision reaches the ~90 call sites that build commands without any of them growing a parameter: the spawn path attaches the runner it chose to the task's context, and every command built from that context (or one derived from it) lands wherever the runner points. A context with no runner attached — which is every context in ty today — resolves to the local runner, so nothing changes for anyone who has not installed a placement handler.

func WorkflowStepFinished added in v0.3.16

func WorkflowStepFinished(worktreePath, baseCommit, baseDirty string) bool

WorkflowStepFinished reports whether a workflow step completed its handoff: produced a commit beyond its recorded baseline, pushed it, and left no uncommitted work of its own. See WorkflowStepUnfinishedReason for the individual checks.

func WorkflowStepUnfinishedReason added in v0.3.16

func WorkflowStepUnfinishedReason(worktreePath, baseCommit, baseDirty string) string

WorkflowStepUnfinishedReason returns "" when the step's handoff is complete, or a short human-readable reason why it is not. The reason is surfaced on the task's activity log so a parked step says WHY it parked ("left uncommitted files: …") instead of the generic "Waiting for user input" — which reads as a question and sends a human hunting for one that was never asked.

func WorktreeAllowExternalWrites added in v0.3.2

func WorktreeAllowExternalWrites(projectDir string) []string

WorktreeAllowExternalWrites returns the configured allowlist of external write prefixes for a project, or nil if none is configured.

func WorktreeMCPConfigPath added in v0.3.31

func WorktreeMCPConfigPath(taskID int64) string

WorktreeMCPConfigPath is the per-task MCP config file handed to Claude with --mcp-config. Exported for `ty doctor`; see worktreeMCPConfigPath.

Types

type APIError added in v0.3.22

type APIError struct {
	Status    int    // HTTP status, when the transcript recorded one (e.g. 429)
	Transient bool   // the provider's own hint that a retry may succeed
	Message   string // the error text shown to the agent
}

APIError is a provider-side failure recorded in a Claude session transcript: a rate limit, an auth rejection, an overloaded upstream.

func LastAPIError added in v0.3.22

func LastAPIError(transcriptPath string) *APIError

LastAPIError returns the provider error a session ENDED on, or nil.

Why this exists: when the model provider rejects a request, the agent's turn stops and ty parks the task with "Waiting for user input" — a message that reads as a question the agent never asked. The real cause is recorded only in the session transcript, so a step killed by a rate limit looks identical on the board to one genuinely waiting on a human. That cost a real debugging session: a workflow step ran three times, committed nothing each time, and looked like a bad prompt; the transcript said

API Error: Request rejected (429) · you have reached your session usage limit

An error is only reported if the session ended on it. Claude Code retries transient failures, so an error followed by a successful assistant response is noise — reporting it would train the reader to ignore these lines.

func (*APIError) String added in v0.3.22

func (e *APIError) String() string

String renders the error for a task's activity log.

type CarryReport added in v0.3.28

type CarryReport struct {
	Branch string
	Commit string
	// WIPCommit is true when uncommitted work had to be committed to travel.
	WIPCommit bool
	// LeftBehind are git-ignored files present in the worktree. They do NOT
	// move. Naming them is the whole point: an ignored file is discovered as an
	// absence on the far side, hours later, and never as an error.
	LeftBehind []string
}

CarryReport is what a carry did, for the human who asked for it.

func CarryWork added in v0.3.28

func CarryWork(ctx context.Context, src WorkSource, handoff, destination string) (CarryReport, error)

CarryWork puts every tracked file in the task's worktree onto its branch and pushes it, so the work exists somewhere both hosts can reach.

It is deliberately paranoid at the end. Committing and pushing can each fail in ways that leave a plausible-looking worktree behind — a pre-commit hook that exits non-zero, a push rejected for being behind, a detached HEAD that swallows the commit where no branch will ever find it. So the last thing this does is re-read the state and prove the two facts that matter: nothing is uncommitted, and the branch tip is on the remote. Callers must treat an error here as "the move does not happen", because the alternative is a move that silently drops the work it promised to carry.

type ClaudeExecutor

type ClaudeExecutor struct {
	// contains filtered or unexported fields
}

ClaudeExecutor implements TaskExecutor for Claude Code CLI. This wraps the existing Claude execution logic in executor.go.

func NewClaudeExecutor

func NewClaudeExecutor(e *Executor) *ClaudeExecutor

NewClaudeExecutor creates a new Claude executor.

func (*ClaudeExecutor) BuildCommand

func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

func (*ClaudeExecutor) Execute

func (c *ClaudeExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using Claude Code CLI.

func (*ClaudeExecutor) FindSessionID

func (c *ClaudeExecutor) FindSessionID(workDir string) string

FindSessionID discovers the most recent Claude session ID for the given workDir.

func (*ClaudeExecutor) GetProcessID

func (c *ClaudeExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the Claude process for a task.

func (*ClaudeExecutor) IsAvailable

func (c *ClaudeExecutor) IsAvailable() bool

IsAvailable checks if the claude CLI is installed.

func (*ClaudeExecutor) Kill

func (c *ClaudeExecutor) Kill(taskID int64) bool

Kill terminates the Claude process for a task.

func (*ClaudeExecutor) Name

func (c *ClaudeExecutor) Name() string

Name returns the executor name.

func (*ClaudeExecutor) Resume

func (c *ClaudeExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume resumes a previous Claude session with feedback.

func (*ClaudeExecutor) ResumeDangerous

func (c *ClaudeExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous kills the current Claude process and restarts with --dangerously-skip-permissions.

func (*ClaudeExecutor) ResumeSafe

func (c *ClaudeExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe kills the current Claude process and restarts without --dangerously-skip-permissions.

func (*ClaudeExecutor) SessionExists added in v0.3.12

func (c *ClaudeExecutor) SessionExists(task *db.Task, sessionID string) bool

SessionExists reports whether the stored Claude session file still exists on disk for this task, resolving the same per-project config dir and worktree the resume command would use. Claude prunes session JSONLs after cleanupPeriodDays (default 30), so a long-parked task's stored session ID often points at a deleted file; EnsureTaskWindow uses this to start fresh instead of resuming into a dead "lost executor pane".

func (*ClaudeExecutor) SupportsDangerousMode

func (c *ClaudeExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns true - Claude supports --dangerously-skip-permissions.

func (*ClaudeExecutor) SupportsSessionResume

func (c *ClaudeExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true - Claude supports session resume via --resume.

type CodexExecutor

type CodexExecutor struct {
	// contains filtered or unexported fields
}

CodexExecutor implements TaskExecutor for OpenAI's Codex CLI.

func NewCodexExecutor

func NewCodexExecutor(e *Executor) *CodexExecutor

NewCodexExecutor creates a new Codex executor.

func (*CodexExecutor) BuildCommand

func (c *CodexExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive Codex session.

func (*CodexExecutor) Execute

func (c *CodexExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using Codex CLI.

func (*CodexExecutor) FindSessionID

func (c *CodexExecutor) FindSessionID(workDir string) string

FindSessionID discovers the most recent Codex session ID for the given workDir. Codex stores sessions in ~/.codex/sessions/ with project-specific subdirectories.

func (*CodexExecutor) GetProcessID

func (c *CodexExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the Codex process for a task.

func (*CodexExecutor) IsAvailable

func (c *CodexExecutor) IsAvailable() bool

IsAvailable checks if the codex CLI is installed.

func (*CodexExecutor) Kill

func (c *CodexExecutor) Kill(taskID int64) bool

Kill terminates the Codex process for a task.

func (*CodexExecutor) Name

func (c *CodexExecutor) Name() string

Name returns the executor name.

func (*CodexExecutor) Resume

func (c *CodexExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume resumes a previous Codex session with feedback.

func (*CodexExecutor) ResumeDangerous

func (c *CodexExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous kills the current Codex process and restarts with --dangerously-bypass-approvals-and-sandbox.

func (*CodexExecutor) ResumeSafe

func (c *CodexExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe kills the current Codex process and restarts without the dangerous flag.

func (*CodexExecutor) SupportsDangerousMode

func (c *CodexExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns true - Codex supports --dangerously-bypass-approvals-and-sandbox.

func (*CodexExecutor) SupportsSessionResume

func (c *CodexExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true - Codex supports session resume via --resume.

type CursorExecutor added in v0.3.27

type CursorExecutor struct {
	// contains filtered or unexported fields
}

CursorExecutor implements TaskExecutor for the Cursor Agent CLI. See: https://cursor.com/docs/cli and https://cursor.com/install

CLI reference (agent --help / cursor.com/docs/cli/reference/parameters):

  • agent "prompt" Interactive agent with an initial prompt
  • agent --force / --yolo Force-allow commands (dangerous mode)
  • agent --resume [chatId] Resume a chat session
  • agent --continue Continue the most recent session
  • agent --model <model> Model to use
  • agent --approve-mcps Auto-approve project MCP servers

TaskYou already creates an isolated git worktree per task, so we never pass Cursor's own --worktree flag (that would nest a second worktree).

func NewCursorExecutor added in v0.3.27

func NewCursorExecutor(e *Executor) *CursorExecutor

NewCursorExecutor creates a new Cursor executor.

func (*CursorExecutor) BuildCommand added in v0.3.27

func (c *CursorExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive Cursor session.

func (*CursorExecutor) Execute added in v0.3.27

func (c *CursorExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using the Cursor CLI.

func (*CursorExecutor) FindSessionID added in v0.3.27

func (c *CursorExecutor) FindSessionID(workDir string) string

FindSessionID discovers the most recent Cursor session ID for the given workDir.

func (*CursorExecutor) GetProcessID added in v0.3.27

func (c *CursorExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the Cursor process for a task.

func (*CursorExecutor) IsAvailable added in v0.3.27

func (c *CursorExecutor) IsAvailable() bool

IsAvailable checks if the Cursor Agent CLI is installed.

func (*CursorExecutor) Kill added in v0.3.27

func (c *CursorExecutor) Kill(taskID int64) bool

Kill terminates the Cursor process for a task.

func (*CursorExecutor) Name added in v0.3.27

func (c *CursorExecutor) Name() string

Name returns the executor name.

func (*CursorExecutor) Resume added in v0.3.27

func (c *CursorExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume continues a previous Cursor session, appending feedback as the next prompt.

func (*CursorExecutor) ResumeDangerous added in v0.3.27

func (c *CursorExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous kills the current Cursor process and restarts with --force.

func (*CursorExecutor) ResumeSafe added in v0.3.27

func (c *CursorExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe kills the current Cursor process and restarts without --force.

func (*CursorExecutor) SupportsDangerousMode added in v0.3.27

func (c *CursorExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns true — Cursor supports --force / --yolo.

func (*CursorExecutor) SupportsSessionResume added in v0.3.27

func (c *CursorExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true — Cursor supports --resume by chat ID.

type ExecResult

type ExecResult struct {
	Success     bool   // Task completed successfully
	NeedsInput  bool   // Task is waiting for user input
	Interrupted bool   // Task was interrupted by user
	Message     string // Status message or error
	Requeued    bool   // Task was deliberately re-queued while running; caller must preserve queued, not write backlog
}

ExecResult represents the result of executing a task.

type Executor

type Executor struct {
	// contains filtered or unexported fields
}

Executor manages background task execution.

func New

func New(database *db.DB, cfg *config.Config) *Executor

New creates a new executor.

func NewWithLogging

func NewWithLogging(database *db.DB, cfg *config.Config, w io.Writer) *Executor

NewWithLogging creates an executor that logs to stderr (for daemon mode).

func (*Executor) AllExecutors

func (e *Executor) AllExecutors() []string

AllExecutors returns the names of all registered executors.

func (*Executor) ArchiveWorktree

func (e *Executor) ArchiveWorktree(task *db.Task) error

ArchiveWorktree saves the complete worktree state (including uncommitted changes) to a git ref and then removes the worktree. This allows the task to be unarchived later with all changes restored.

The archive process: 1. Saves all changes (committed, uncommitted, tracked, untracked) to a git ref 2. Stores the archive state in the database 3. Runs teardown script 4. Removes the worktree with --force

func (*Executor) AuditWorktreePaths added in v0.3.30

func (e *Executor) AuditWorktreePaths(fix bool) ([]WorktreePathIssue, error)

AuditWorktreePaths finds task rows whose worktree_path (or the archive_worktree_path an unarchive would restore to) names the project's main checkout rather than a linked worktree. Those rows are the ones that make the stale-worktree sweeper attempt an impossible `git worktree remove`, and they leave a real repo checkout recorded where every caller expects a disposable worktree.

With fix set, the offending references are cleared. Tasks that are currently running are reported but never modified — whatever their path says, something is using it right now.

func (*Executor) AvailableExecutors

func (e *Executor) AvailableExecutors() []string

AvailableExecutors returns the names of all available executors.

func (*Executor) CleanupDuplicateWindows

func (e *Executor) CleanupDuplicateWindows(taskID int64)

CleanupDuplicateWindows removes duplicate tmux windows for a task, keeping only the canonical one. This prevents window proliferation that can occur from repeated break-pane operations.

func (*Executor) CleanupStaleWorktreesManual

func (e *Executor) CleanupStaleWorktreesManual(maxAge time.Duration, dryRun bool) ([]*db.Task, error)

CleanupStaleWorktreesManual runs stale worktree cleanup on demand and returns the list of tasks that were cleaned up. If dryRun is true, no changes are made.

func (*Executor) CleanupWorktree

func (e *Executor) CleanupWorktree(task *db.Task) error

CleanupWorktree removes a task's worktree.

func (*Executor) DisplayName

func (e *Executor) DisplayName() string

DisplayName returns the configured executor display name.

func (*Executor) EnsureLocalWorktree added in v0.3.28

func (e *Executor) EnsureLocalWorktree(task *db.Task) (string, bool, error)

EnsureLocalWorktree gives a task its isolated worktree on THIS machine, returning the directory and whether this call created it.

It exists so a task can be prepared at the moment it is placed here rather than only at the moment the daemon happens to start it. `ty place <id> local` used to write a placement and stop: the task arrived with an empty worktree_path, and every start path that is not the daemon — the TUI, the GUI, the HTTP API — refused it with "task has no worktree yet: refusing to start outside an isolated worktree". The guard was right; nothing had done the provisioning it was guarding.

func (*Executor) EnsureTaskWindow added in v0.3.8

func (e *Executor) EnsureTaskWindow(ctx context.Context, task *db.Task, sessionID, handoffContext string) (string, bool, error)

EnsureTaskWindow ensures a tmux window running the task's executor session exists in a task-daemon session, creating the daemon session and window if needed. It is shared by the TUI detail view and the HTTP API so the interactive-session bootstrap logic lives in exactly one place.

sessionID optionally resumes an existing executor session (e.g. a stored Claude session ID). handoffContext is prepended to the prompt when starting fresh (used when switching executors mid-task).

Returns the tmux window target and whether a new window was created. For an existing window the target is "session:index"; for a freshly created one it is "session:windowName".

func (*Executor) ExecuteNow

func (e *Executor) ExecuteNow(ctx context.Context, taskID int64) error

ExecuteNow runs a task immediately (blocking).

func (*Executor) ExecutorSlug

func (e *Executor) ExecutorSlug() string

ExecutorSlug returns the normalized identifier for the executor (e.g., "codex").

func (*Executor) GetExecutor

func (e *Executor) GetExecutor(name string) TaskExecutor

GetExecutor returns the executor for a task by name.

func (*Executor) GetProjectDir

func (e *Executor) GetProjectDir(project string) string

GetProjectDir returns the directory for a project (exported for UI).

func (*Executor) GetTaskExecutor

func (e *Executor) GetTaskExecutor(task *db.Task) TaskExecutor

GetTaskExecutor returns the executor for a specific task.

func (*Executor) Interrupt

func (e *Executor) Interrupt(taskID int64) bool

Interrupt cancels a running task. If running in this process, cancels directly. Also marks in DB for cross-process interrupt.

func (*Executor) IsClaudeRunning

func (e *Executor) IsClaudeRunning(taskID int64) bool

IsClaudeRunning checks if a Claude process is running for a task.

func (*Executor) IsRunning

func (e *Executor) IsRunning(taskID int64) bool

IsRunning checks if a specific task is currently executing.

func (*Executor) KillClaudeProcess

func (e *Executor) KillClaudeProcess(taskID int64) bool

Exported for use by the UI when deleting tasks.

func (*Executor) KillPiProcess

func (e *Executor) KillPiProcess(taskID int64) bool

KillPiProcess terminates the Pi process for a task to free up memory.

func (*Executor) NotifyTaskChange

func (e *Executor) NotifyTaskChange(eventType string, task *db.Task)

NotifyTaskChange notifies subscribers of a task change (for use by UI/other components). If the task is newly queued, it also triggers immediate processing so the executor starts without waiting for the next poll cycle.

func (*Executor) RemoteLocation added in v0.3.28

func (e *Executor) RemoteLocation(task *db.Task) (RemoteTaskLocation, bool)

RemoteLocation returns where a task is running when it was placed on another host, and false when it is an ordinary local task.

func (*Executor) RemoteSessionState added in v0.3.28

func (e *Executor) RemoteSessionState(ctx context.Context, task *db.Task) RemoteSessionStatus

RemoteSessionState reports whether a remotely placed task still has a live tmux window on its host.

It is the same probe the poller runs, deliberately: a surface that decides whether to attach must not be able to disagree with the daemon that decides whether the task is finished.

func (*Executor) RenameClaudeSessionForTask

func (e *Executor) RenameClaudeSessionForTask(task *db.Task, newName string)

RenameClaudeSessionForTask renames the Claude session for a task if it has a worktree. This is a convenience method that handles the common case of renaming based on task.

func (*Executor) ResumeDangerous

func (e *Executor) ResumeDangerous(taskID int64) bool

ResumeDangerous kills the current executor process and restarts it with dangerous mode enabled. This allows switching a running task to dangerous mode without restarting the daemon. Returns true if successfully restarted, false otherwise. This is executor-aware and will delegate to the appropriate executor's implementation.

func (*Executor) ResumeSafe

func (e *Executor) ResumeSafe(taskID int64) bool

ResumeSafe kills the current executor process and restarts it with dangerous mode disabled. This allows switching a running task back from dangerous mode to safe mode. Returns true if successfully restarted, false otherwise. This is executor-aware and will delegate to the appropriate executor's implementation.

func (*Executor) ResumeWithMode added in v0.3.2

func (e *Executor) ResumeWithMode(taskID int64, mode string) bool

ResumeWithMode persists a new permission mode for a running task and relaunches its agent so the live session matches the stored mode. This is the single entry point for changing a running task's permission mode: it writes permission_mode (the source of truth) first so the resume builds the right CLI flag, then routes to the dangerous or non-dangerous resume path. On failure it rolls the stored mode back so the badge never claims a mode the live session isn't actually in.

func (*Executor) RunningTasks

func (e *Executor) RunningTasks() []int64

RunningTasks returns the IDs of currently processing tasks.

func (*Executor) Start

func (e *Executor) Start(ctx context.Context)

Start begins the background worker.

func (*Executor) Stop

func (e *Executor) Stop()

Stop stops the background worker.

func (*Executor) Subscribe

func (e *Executor) Subscribe(taskID int64) chan *db.TaskLog

Subscribe to log updates for a task.

func (*Executor) SubscribeTaskEvents

func (e *Executor) SubscribeTaskEvents() chan TaskEvent

SubscribeTaskEvents subscribes to task change events (status changes, etc.).

func (*Executor) SuspendTaskSession added in v0.3.30

func (e *Executor) SuspendTaskSession(taskID int64) bool

SuspendTaskSession suspends a task by tearing down its agent: the tmux window is killed (taking the agent process with it) and the task's tmux placement is cleared, while claude_session_id is preserved so `ty retry` — or simply reopening the task — resumes the conversation with `--resume`.

This is what actually reclaims memory. The previous implementation sent SIGTSTP, which stops the process but leaves every page of it resident, so it returned CPU and no RAM. A parked agent holding hundreds of megabytes is exactly what the idle sweep exists to reclaim.

Shared with `ty sessions suspend` so the manual and automatic paths cannot drift on what "suspended" means. Returns true if a window was killed.

func (*Executor) TriggerProcessing

func (e *Executor) TriggerProcessing()

TriggerProcessing wakes up the worker loop to process queued tasks immediately. This is a non-blocking operation; if a wakeup is already pending, the call is a no-op.

func (*Executor) UnarchiveWorktree

func (e *Executor) UnarchiveWorktree(task *db.Task) error

UnarchiveWorktree recreates a worktree from saved archive state and restores all changes (including uncommitted changes that were saved during archiving).

The unarchive process: 1. Recreates the worktree (handling edge cases like branch deleted, dir taken, etc.) 2. Restores changes from the archive ref 3. Runs init script 4. Clears the archive state from the database

func (*Executor) Unsubscribe

func (e *Executor) Unsubscribe(taskID int64, ch chan *db.TaskLog)

Unsubscribe from log updates.

func (*Executor) UnsubscribeTaskEvents

func (e *Executor) UnsubscribeTaskEvents(ch chan TaskEvent)

UnsubscribeTaskEvents unsubscribes from task events.

type ExecutorFactory

type ExecutorFactory struct {
	// contains filtered or unexported fields
}

ExecutorFactory manages creation of task executors.

func NewExecutorFactory

func NewExecutorFactory() *ExecutorFactory

NewExecutorFactory creates a new executor factory.

func (*ExecutorFactory) All

func (f *ExecutorFactory) All() []string

All returns all registered executor names.

func (*ExecutorFactory) Available

func (f *ExecutorFactory) Available() []string

Available returns the names of all registered executors that are available.

func (*ExecutorFactory) Get

func (f *ExecutorFactory) Get(name string) TaskExecutor

Get returns the executor for the given name, or nil if not found.

func (*ExecutorFactory) Register

func (f *ExecutorFactory) Register(executor TaskExecutor)

Register adds an executor to the factory.

type GeminiExecutor

type GeminiExecutor struct {
	// contains filtered or unexported fields
}

GeminiExecutor implements TaskExecutor for Google's Gemini CLI.

func NewGeminiExecutor

func NewGeminiExecutor(e *Executor) *GeminiExecutor

NewGeminiExecutor creates a new Gemini executor.

func (*GeminiExecutor) BuildCommand

func (g *GeminiExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive Gemini session.

func (*GeminiExecutor) Execute

func (g *GeminiExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using the Gemini CLI.

func (*GeminiExecutor) FindSessionID

func (g *GeminiExecutor) FindSessionID(workDir string) string

FindSessionID discovers the most recent Gemini session ID for the given workDir. Gemini stores sessions in ~/.gemini/tmp/<project_hash>/chats/ directory.

func (*GeminiExecutor) GetProcessID

func (g *GeminiExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the Gemini process for a task.

func (*GeminiExecutor) IsAvailable

func (g *GeminiExecutor) IsAvailable() bool

IsAvailable checks if the gemini CLI is installed.

func (*GeminiExecutor) Kill

func (g *GeminiExecutor) Kill(taskID int64) bool

Kill terminates the Gemini process for a task.

func (*GeminiExecutor) Name

func (g *GeminiExecutor) Name() string

Name returns the executor name.

func (*GeminiExecutor) Resume

func (g *GeminiExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume runs Gemini again with the full prompt plus feedback since sessions are stateless.

func (*GeminiExecutor) ResumeDangerous

func (g *GeminiExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous kills the current Gemini process and restarts with --dangerously-allow-run.

func (*GeminiExecutor) ResumeSafe

func (g *GeminiExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe kills the current Gemini process and restarts without the dangerous flag.

func (*GeminiExecutor) SupportsDangerousMode

func (g *GeminiExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns true - Gemini supports --dangerously-allow-run.

func (*GeminiExecutor) SupportsSessionResume

func (g *GeminiExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true - Gemini supports session resume via --resume.

type GrokExecutor added in v0.3.24

type GrokExecutor struct {
	// contains filtered or unexported fields
}

GrokExecutor implements TaskExecutor for the Grok CLI (xAI / SpaceXAI). See: https://x.ai/cli/install.sh and grok --help

CLI reference (grok --help):

  • grok "prompt" Interactive TUI with an initial prompt
  • grok --always-approve Auto-approve tool executions (dangerous mode)
  • grok --permission-mode M default | acceptEdits | auto | dontAsk | bypassPermissions | plan
  • grok --resume [SESSION] Resume a session by ID (or most recent)
  • grok --continue Continue the most recent session for this cwd

TaskYou already creates an isolated git worktree per task, so we never pass grok's own --worktree flag (that would nest a second worktree).

func NewGrokExecutor added in v0.3.24

func NewGrokExecutor(e *Executor) *GrokExecutor

NewGrokExecutor creates a new Grok executor.

func (*GrokExecutor) BuildCommand added in v0.3.24

func (g *GrokExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive Grok session.

func (*GrokExecutor) Execute added in v0.3.24

func (g *GrokExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using the Grok CLI.

func (*GrokExecutor) FindSessionID added in v0.3.24

func (g *GrokExecutor) FindSessionID(workDir string) string

FindSessionID discovers the most recent Grok session ID for the given workDir.

func (*GrokExecutor) GetProcessID added in v0.3.24

func (g *GrokExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the Grok process for a task.

func (*GrokExecutor) IsAvailable added in v0.3.24

func (g *GrokExecutor) IsAvailable() bool

IsAvailable checks if the grok CLI is installed.

func (*GrokExecutor) Kill added in v0.3.24

func (g *GrokExecutor) Kill(taskID int64) bool

Kill terminates the Grok process for a task.

func (*GrokExecutor) Name added in v0.3.24

func (g *GrokExecutor) Name() string

Name returns the executor name.

func (*GrokExecutor) Resume added in v0.3.24

func (g *GrokExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume continues a previous Grok session, appending feedback as the next prompt.

func (*GrokExecutor) ResumeDangerous added in v0.3.24

func (g *GrokExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous kills the current Grok process and restarts with --always-approve.

func (*GrokExecutor) ResumeSafe added in v0.3.24

func (g *GrokExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe kills the current Grok process and restarts without --always-approve.

func (*GrokExecutor) SupportsDangerousMode added in v0.3.24

func (g *GrokExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns true — Grok supports --always-approve.

func (*GrokExecutor) SupportsSessionResume added in v0.3.24

func (g *GrokExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true — Grok supports --resume by session ID.

type HandoffTask added in v0.3.28

type HandoffTask struct {
	ID    int64
	Title string
	Host  string
}

HandoffTask is the task detail a synthesised handoff can honestly state. It is deliberately small: everything else ty knows about a task is already in the task's own description on the far side.

type LocalRunner added in v0.3.28

type LocalRunner struct{}

LocalRunner runs commands on this machine, in this process's environment.

Command is deliberately a one-liner: it must stay byte-for-byte what the call sites used to do inline, so that routing them through the seam cannot change what ty does.

func (LocalRunner) Command added in v0.3.28

func (LocalRunner) Command(ctx context.Context, workDir, name string, args ...string) *exec.Cmd

Command builds a local command, run in workDir. An empty workDir leaves the process in the caller's working directory, matching a bare exec.Command.

func (LocalRunner) Target added in v0.3.28

func (LocalRunner) Target() string

Target is "" — local execution has no remote to name.

type OpenClawExecutor

type OpenClawExecutor struct {
	// contains filtered or unexported fields
}

OpenClawExecutor implements TaskExecutor for OpenClaw AI assistant. OpenClaw is an open-source personal AI assistant that runs locally, capable of automating tasks across your digital life. See: https://openclaw.ai

CLI Reference (using openclaw tui for interactive terminal UI):

  • openclaw tui --session <key> - Connect to a session
  • openclaw tui --message "prompt" - Send initial message after connecting
  • openclaw tui --thinking <level> - Set reasoning depth (off/minimal/low/medium/high)

func NewOpenClawExecutor

func NewOpenClawExecutor(e *Executor) *OpenClawExecutor

NewOpenClawExecutor creates a new OpenClaw executor.

func (*OpenClawExecutor) BuildCommand

func (o *OpenClawExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive OpenClaw session.

func (*OpenClawExecutor) Execute

func (o *OpenClawExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using the OpenClaw CLI.

func (*OpenClawExecutor) FindSessionID

func (o *OpenClawExecutor) FindSessionID(workDir string) string

FindSessionID returns the session key for the given task. OpenClaw uses explicit session keys rather than auto-discovering from files.

func (*OpenClawExecutor) GetProcessID

func (o *OpenClawExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the OpenClaw process for a task.

func (*OpenClawExecutor) IsAvailable

func (o *OpenClawExecutor) IsAvailable() bool

IsAvailable checks if the openclaw CLI is installed.

func (*OpenClawExecutor) Kill

func (o *OpenClawExecutor) Kill(taskID int64) bool

Kill terminates the OpenClaw process for a task.

func (*OpenClawExecutor) Name

func (o *OpenClawExecutor) Name() string

Name returns the executor name.

func (*OpenClawExecutor) Resume

func (o *OpenClawExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume resumes a previous session using OpenClaw's --session-id flag. If no session ID exists, starts fresh with the full prompt + feedback.

func (*OpenClawExecutor) ResumeDangerous

func (o *OpenClawExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous is not supported for OpenClaw as it doesn't have a dangerous mode flag.

func (*OpenClawExecutor) ResumeSafe

func (o *OpenClawExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe is not supported for OpenClaw as it doesn't have a dangerous mode flag.

func (*OpenClawExecutor) SupportsDangerousMode

func (o *OpenClawExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns false - OpenClaw does not currently have a dangerous mode flag. Users can configure OpenClaw's permissions through its own settings.

func (*OpenClawExecutor) SupportsSessionResume

func (o *OpenClawExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true - OpenClaw supports session resume via --session.

type OpenCodeExecutor

type OpenCodeExecutor struct {
	// contains filtered or unexported fields
}

OpenCodeExecutor implements TaskExecutor for OpenCode AI assistant. OpenCode is an open-source AI coding assistant that runs in terminal, IDE, or desktop. It supports multiple LLM providers including Claude, GPT, and Gemini. See: https://opencode.ai

CLI Reference:

  • opencode - Start interactive TUI session
  • Supports @-symbol fuzzy search for file references
  • Plan mode (Tab) for review before implementation
  • /init - Analyze project and generate AGENTS.md
  • /undo, /redo - Revert or restore changes
  • /share - Generate shareable conversation links

func NewOpenCodeExecutor

func NewOpenCodeExecutor(e *Executor) *OpenCodeExecutor

NewOpenCodeExecutor creates a new OpenCode executor.

func (*OpenCodeExecutor) BuildCommand

func (o *OpenCodeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive OpenCode session.

func (*OpenCodeExecutor) Execute

func (o *OpenCodeExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using the OpenCode CLI.

func (*OpenCodeExecutor) FindSessionID

func (o *OpenCodeExecutor) FindSessionID(workDir string) string

FindSessionID returns empty - OpenCode doesn't support session discovery.

func (*OpenCodeExecutor) GetProcessID

func (o *OpenCodeExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the OpenCode process for a task.

func (*OpenCodeExecutor) IsAvailable

func (o *OpenCodeExecutor) IsAvailable() bool

IsAvailable checks if the opencode CLI is installed.

func (*OpenCodeExecutor) Kill

func (o *OpenCodeExecutor) Kill(taskID int64) bool

Kill terminates the OpenCode process for a task.

func (*OpenCodeExecutor) Name

func (o *OpenCodeExecutor) Name() string

Name returns the executor name.

func (*OpenCodeExecutor) Resume

func (o *OpenCodeExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume resumes a previous session with feedback. OpenCode doesn't have explicit session resumption, so we replay full prompt + feedback.

func (*OpenCodeExecutor) ResumeDangerous

func (o *OpenCodeExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous is not supported for OpenCode as it doesn't have a dangerous mode flag.

func (*OpenCodeExecutor) ResumeSafe

func (o *OpenCodeExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe is not supported for OpenCode as it doesn't have a dangerous mode flag.

func (*OpenCodeExecutor) SupportsDangerousMode

func (o *OpenCodeExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns false - OpenCode does not currently have a dangerous mode flag. It runs in an interactive TUI and requires user interaction for actions.

func (*OpenCodeExecutor) SupportsSessionResume

func (o *OpenCodeExecutor) SupportsSessionResume() bool

SupportsSessionResume returns false - OpenCode doesn't have explicit session resume. Conversations can be shared via /share but not resumed from command line.

type PiExecutor

type PiExecutor struct {
	// contains filtered or unexported fields
}

PiExecutor implements TaskExecutor for Pi coding agent.

func NewPiExecutor

func NewPiExecutor(e *Executor) *PiExecutor

NewPiExecutor creates a new Pi executor.

func (*PiExecutor) BuildCommand

func (p *PiExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string

BuildCommand returns the shell command to start an interactive Pi session.

func (*PiExecutor) Execute

func (p *PiExecutor) Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

Execute runs a task using Pi.

func (*PiExecutor) FindSessionID

func (p *PiExecutor) FindSessionID(workDir string) string

FindSessionID discovers the most recent Pi session ID for the given workDir.

func (*PiExecutor) GetProcessID

func (p *PiExecutor) GetProcessID(taskID int64) int

GetProcessID returns the PID of the Pi process for a task.

func (*PiExecutor) IsAvailable

func (p *PiExecutor) IsAvailable() bool

IsAvailable checks if the pi CLI is installed.

func (*PiExecutor) Kill

func (p *PiExecutor) Kill(taskID int64) bool

Kill terminates the Pi process for a task.

func (*PiExecutor) Name

func (p *PiExecutor) Name() string

Name returns the executor name.

func (*PiExecutor) Resume

func (p *PiExecutor) Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

Resume resumes a previous Pi session with feedback.

func (*PiExecutor) ResumeDangerous

func (p *PiExecutor) ResumeDangerous(task *db.Task, workDir string) bool

ResumeDangerous is not supported for Pi.

func (*PiExecutor) ResumeSafe

func (p *PiExecutor) ResumeSafe(task *db.Task, workDir string) bool

ResumeSafe is not supported for Pi.

func (*PiExecutor) SupportsDangerousMode

func (p *PiExecutor) SupportsDangerousMode() bool

SupportsDangerousMode returns false - Pi doesn't have a dangerous mode flag.

func (*PiExecutor) SupportsSessionResume

func (p *PiExecutor) SupportsSessionResume() bool

SupportsSessionResume returns true - Pi supports session resume via --continue.

type PlacementConfig added in v0.3.28

type PlacementConfig struct {
	RemoteRequired bool `yaml:"remote_required"`
}

PlacementConfig controls whether a project may fall back to this machine.

type PlacementResult added in v0.3.28

type PlacementResult struct {
	Messages []string `json:"messages"`
}

PlacementResult contains user-facing progress and warnings from a completed move.

func PlaceTask added in v0.3.28

func PlaceTask(ctx context.Context, database *db.DB, taskID int64, target, workDir string, force bool) (PlacementResult, error)

PlaceTask carries work and changes placement through one service for all UIs.

type ProjectConfig

type ProjectConfig struct {
	Worktree  WorktreeConfig  `yaml:"worktree"`
	Placement PlacementConfig `yaml:"placement"`
}

ProjectConfig represents the .taskyou.yml configuration file in a project root.

func LoadProjectConfig

func LoadProjectConfig(projectDir string) (*ProjectConfig, error)

LoadProjectConfig loads the project configuration from the given directory. It returns nil if no configuration file exists.

type RemoteRunner added in v0.3.28

type RemoteRunner struct {
	// Host is the ssh destination, exactly as the placement handler named it —
	// an ssh_config alias, a user@host, whatever the operator's fleet uses.
	Host string
	// WorkDir is the task's directory on that host, used when a command does not
	// name one of its own. It is a REMOTE path: a leading "~" is deliberately
	// left unexpanded so the remote shell expands it, not this machine's.
	WorkDir string
	// SSHBin is the ssh binary; empty means "ssh".
	SSHBin string
	// ConnectTimeout bounds the TCP/handshake phase. Zero means
	// DefaultRemoteConnectTimeout.
	ConnectTimeout time.Duration
}

RemoteRunner runs commands on another machine over ssh.

It is the second implementation of the Runner seam, and the only thing in ty that knows a task can run somewhere other than here. It still knows nothing about hosts: the host name and the directory come from a task.placement handler's answer, verbatim. Choosing them is the extension's job; reaching them is this type's.

func (RemoteRunner) Command added in v0.3.28

func (r RemoteRunner) Command(ctx context.Context, workDir, name string, args ...string) *exec.Cmd

Command builds an ssh invocation that runs name+args on the remote host.

workDir is a REMOTE directory. It is applied with a `cd` inside the remote shell rather than with cmd.Dir, which would point at a path on this machine — the single most likely way to get this subtly, silently wrong.

func (RemoteRunner) Preflight added in v0.3.28

func (r RemoteRunner) Preflight(ctx context.Context) (string, error)

Preflight checks that the host answers, that it has tmux, and that the placement's workdir exists on it — and returns that workdir resolved to an absolute remote path.

The resolution is not a nicety. Inventories are written in the remote user's own terms ("~/projects/engineering"), and tmux's -c flag does no tilde expansion, so a window created with the raw answer would fail to start in the right directory. Asking the remote shell once, up front, is the only place that expansion can honestly happen.

A failure here is NOT a reason to fall back to local. Failing to DECIDE where to run falls back; failing to RUN where you were told does not — a silent fallback would put the load right back on the machine this feature exists to unload, on exactly the days nobody is watching.

func (RemoteRunner) Target added in v0.3.28

func (r RemoteRunner) Target() string

Target names the host this runner reaches, which is what makes a placement visible in `ty show` and on the board.

type RemoteSessionStatus added in v0.3.28

type RemoteSessionStatus int

RemoteSessionStatus is what a local surface found when it looked for a remotely placed task's live tmux window.

const (
	// RemoteSessionLive means the window is there and can be attached to.
	RemoteSessionLive RemoteSessionStatus = iota
	// RemoteSessionEnded means the host answered and the window is not there.
	RemoteSessionEnded
	// RemoteSessionUnreachable means the host did not answer, so nothing is known
	// about the window one way or the other.
	RemoteSessionUnreachable
)

type RemoteTaskLocation added in v0.3.28

type RemoteTaskLocation struct {
	Host    string // the placed host
	WorkDir string // the task's worktree on it
	Branch  string // the branch checked out there
	Attach  string // the ssh+tmux command that attaches to its session
}

RemoteTaskLocation describes where a remotely placed task actually lives, for surfaces (the TUI, `ty show`) that can only say so rather than show it.

type RemoteTerminal added in v0.3.28

type RemoteTerminal struct {
	WindowTarget string
	AgentPaneID  string
	ShellPaneID  string
}

RemoteTerminal identifies panes on the placed host. These IDs must never be stored in the task's local pane fields or passed to a local tmux server.

func InspectRemoteTerminal added in v0.3.28

func InspectRemoteTerminal(ctx context.Context, task *db.Task, workdir string, ensureShell bool) (RemoteTerminal, error)

InspectRemoteTerminal discovers the live agent and optionally ensures a persistent workdir shell. Both the TUI and HTTP terminal use this operation.

type Runner added in v0.3.28

type Runner interface {
	// Command builds a command to run in workDir.
	Command(ctx context.Context, workDir, name string, args ...string) *exec.Cmd
	// Target is "" for local execution.
	Target() string
}

Runner builds commands for execution in some location.

Every command ty runs to provision a task's workspace (git worktree, clone) or to create and drive the tmux session its agent lives in is built through a Runner rather than by calling exec.Command directly. That is the whole point of the type: without it, "this runs on the local machine" is spelled out at ~90 call sites and cannot be varied; with it, the assumption lives in exactly one implementation.

Today there is one implementation, LocalRunner, and it is exactly the behaviour ty has always had. Nothing selects a different one, and nothing user-facing knows the concept exists.

func DefaultRunner added in v0.3.28

func DefaultRunner() Runner

DefaultRunner returns the Runner used to build every command in the execution path. It is local, always, and there is no configuration that changes that.

This is the seam: choosing where a task runs is a matter of returning a different Runner from here.

func RunnerFrom added in v0.3.28

func RunnerFrom(ctx context.Context) Runner

RunnerFrom returns the Runner attached to ctx, or the default (local) runner.

type TaskEvent

type TaskEvent struct {
	Type   string   // "created", "updated", "deleted", "status_changed"
	Task   *db.Task // The task (may be nil for deleted)
	TaskID int64    // Always set
}

TaskEvent represents a change to a task.

type TaskExecutor

type TaskExecutor interface {
	// Name returns the executor name (e.g., "claude", "codex")
	Name() string

	// Execute runs a task with the given prompt and returns the result.
	// The workDir is the directory where the executor should run.
	Execute(ctx context.Context, task *db.Task, workDir, prompt string) ExecResult

	// Resume resumes a previous session with additional feedback.
	// If no previous session exists, it should start fresh with the full prompt + feedback.
	Resume(ctx context.Context, task *db.Task, workDir, prompt, feedback string) ExecResult

	// BuildCommand returns the shell command to start an interactive session.
	// This is used by the UI to start the executor in a tmux window.
	// The sessionID is optional - if provided, the executor should resume that session if supported.
	// The prompt is the task prompt to pass to the executor.
	BuildCommand(task *db.Task, sessionID, prompt string) string

	// IsAvailable checks if the executor CLI is installed and available.
	IsAvailable() bool

	// GetProcessID returns the PID of the executor process for a task, or 0 if not running.
	GetProcessID(taskID int64) int

	// Kill terminates the executor process for a task.
	Kill(taskID int64) bool

	// SupportsSessionResume returns true if the executor supports resuming previous sessions.
	// If true, FindSessionID and session-based Resume will work.
	SupportsSessionResume() bool

	// SupportsDangerousMode returns true if the executor has a "dangerous mode" flag
	// that skips permission prompts for autonomous operation.
	SupportsDangerousMode() bool

	// FindSessionID discovers and returns the most recent session ID for the given workDir.
	// Returns empty string if no session is found or sessions aren't supported.
	FindSessionID(workDir string) string

	// ResumeDangerous kills the current process and restarts with dangerous mode enabled.
	// Returns true if successfully restarted. Requires session resume support.
	ResumeDangerous(task *db.Task, workDir string) bool

	// ResumeSafe kills the current process and restarts with dangerous mode disabled.
	// Returns true if successfully restarted. Requires session resume support.
	ResumeSafe(task *db.Task, workDir string) bool
}

TaskExecutor defines the interface for task execution backends. Implementations handle the actual running of tasks using different CLI tools.

type WorkSource added in v0.3.28

type WorkSource struct {
	Runner  Runner
	Host    string
	WorkDir string
}

WorkSource is a task's work as it exists right now: a directory on a host, reachable through a Runner. The zero Host means this machine.

func (WorkSource) Where added in v0.3.28

func (s WorkSource) Where() string

Where names the source host for a human.

type WorktreeConfig

type WorktreeConfig struct {
	// InitScript is the path to a script that runs after worktree creation.
	// Can be relative to the project root (e.g., "bin/worktree-setup").
	InitScript string `yaml:"init_script"`
	// TeardownScript is the path to a script that runs before worktree deletion.
	// Can be relative to the project root (e.g., "bin/worktree-teardown").
	TeardownScript string `yaml:"teardown_script"`
	// AllowExternalWrites is an allowlist of path prefixes a task may write to even
	// though they live outside its worktree. Used by the worktree write-guard to
	// pre-approve known-safe external writes (e.g. a shared scratch dir) so they
	// don't prompt. Paths may be absolute or start with ~.
	AllowExternalWrites []string `yaml:"allow_external_writes"`
}

WorktreeConfig contains worktree-specific configuration.

type WorktreeGuardDecision added in v0.3.2

type WorktreeGuardDecision struct {
	Decision string // "ask" or "deny"
	Reason   string
}

WorktreeGuardDecision is the executor-agnostic outcome. The transport layer for each CLI maps it onto that CLI's wire format: Claude/Codex emit hookSpecificOutput.permissionDecision; Gemini emits {decision,reason}; OpenCode throws from its plugin. "ask" is only honored by Claude (whose hook protocol has an interactive permission prompt); executors without one downgrade ask→deny to fail safe.

func EvaluateWorktreeWriteGuard added in v0.3.2

func EvaluateWorktreeWriteGuard(worktreePath string, allowExternal []string, in WorktreeGuardInput) *WorktreeGuardDecision

EvaluateWorktreeWriteGuard returns a decision when the pending tool call would write outside the task's isolated worktree, or nil to let Claude's normal permission flow proceed. worktreePath is the task's worktree root (task.WorktreePath); allowExternal is an optional allowlist of absolute path prefixes that may be written even though they live outside the worktree.

type WorktreeGuardInput added in v0.3.2

type WorktreeGuardInput struct {
	ToolName       string
	ToolInput      json.RawMessage
	Cwd            string
	PermissionMode string
}

WorktreeGuardInput is the canonical, executor-agnostic slice of a pre-tool-use payload the guard needs. Every supported agent CLI (Claude, Codex, Gemini, Grok, Cursor, OpenCode) normalizes its native hook payload into this shape before evaluating the guard, so the policy below has a single source of truth. It is also decoupled from any hook JSON struct so the guard can be unit-tested without constructing full hook payloads.

ToolName uses each executor's real tool vocabulary (e.g. Claude "Write", Codex "apply_patch", Gemini "write_file"/"run_shell_command", Grok "search_replace"/"run_terminal_command"); externalWriteTargets knows how to read the write destinations out of each.

type WorktreePathIssue added in v0.3.30

type WorktreePathIssue struct {
	TaskID  int64
	Title   string
	Project string
	Field   string // "worktree_path" or "archive_worktree_path"
	Path    string
	Reason  string
	Fixed   bool
}

WorktreePathIssue is one task row whose recorded directory is not a linked worktree of its project, so nothing that removes worktrees can ever act on it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL