CommandHandler Specification¶
1. Overview¶
CommandHandler is the unified execution layer for all operational commands in AgentQueue. It is the single code path through which every operation must pass — both Discord slash commands and the LLM chat agent tools delegate their business logic here. Presentation and formatting are handled by the callers; this layer concerns itself only with execution and returning structured results.
The handler holds a reference to an Orchestrator instance (which provides database access and git operations) and an AppConfig. It also maintains a small amount of conversational state: an optional _active_project_id (the currently focused project), and an optional _on_project_deleted callback that external layers (e.g. the Discord bot) can register to react to project deletions.
The db property is a convenience accessor that returns self.orchestrator.db.
Source Files¶
src/command_handler.py
2. Architecture¶
Command Dispatch¶
All commands are executed through a single public method:
Internally, execute looks up a method named _cmd_{name} on the instance using getattr. If found, the method is called with args and its return value is returned. If no such method exists, {"error": "Unknown command: {name}"} is returned. Any unhandled exception raised inside a command method is caught and returned as {"error": str(e)}.
Command Registration¶
Commands are registered implicitly: any instance method named _cmd_<command_name> becomes a callable command. There is no explicit registry or decorator. Adding a new command means defining a new _cmd_ method.
Constructor Signature¶
After construction, callers may set handler._on_project_deleted = callback to register a post-deletion hook (signature: callback(project_id: str) -> None).
3. Return Format¶
Every command returns a dict. The conventions are:
- Success: A dict containing the relevant data. The exact keys vary by command. No
"success"key is used explicitly; the absence of an"error"key signals success. - Error: A dict with a single key
"error"whose value is a human-readable string describing what went wrong.
Examples:
{"created": "my-project", "name": "My Project", "workspace": "/path/to/workspace"}
{"error": "Project 'foo' not found"}
Some commands include both a primary result and an optional "warning" key when a non-fatal concern exists (e.g. IN_PROGRESS tasks during a destructive git operation).
4. Command Categories¶
Status¶
get_status¶
Returns a system-wide snapshot of the orchestrator state.
Parameters: None.
Behavior: Queries all projects, agents, and tasks. For each agent that has a current task, the task's title, project, and status are included. Tasks are filtered into in_progress and ready_to_work lists.
Returns on success:
{
"projects": <int: total project count>,
"agents": [
{
"id": <str>,
"name": <str>,
"state": <str: agent state value>,
"working_on": { # present only if agent has a current task
"task_id": <str>,
"title": <str>,
"project_id": <str>,
"status": <str>,
}
},
...
],
"tasks": {
"total": <int>,
"by_status": {<status_value>: <count>, ...},
"in_progress": [{"id": <str>, "title": <str>, "project_id": <str>, "assigned_agent": <str|None>}, ...],
"ready_to_work": [{"id": <str>, "title": <str>, "project_id": <str>}, ...],
},
"orchestrator_paused": <bool>,
}
Errors: None expected.
Projects¶
list_projects¶
Returns all projects.
Parameters: None.
Behavior: Fetches all projects from the database and returns their core fields. If a project has a Discord channel linked, discord_channel_id is included.
Returns on success:
{
"projects": [
{
"id": <str>,
"name": <str>,
"status": <str>,
"credit_weight": <float>,
"max_concurrent_agents": <int>,
"workspace": <str>,
"discord_channel_id": <str>, # present only if set
},
...
]
}
Errors: None expected.
create_project¶
Creates a new project and its workspace directory.
Parameters:
- name (required): Human-readable project name. The project ID is derived by lowercasing and replacing spaces with hyphens.
- credit_weight (optional, default 1.0): Scheduler weight for this project.
- max_concurrent_agents (optional, default 2): Maximum agents that can work on this project simultaneously.
- auto_create_channels (optional): Boolean override for whether the Discord layer should auto-create a channel. If not provided, falls back to config.discord.per_project_channels.auto_create.
Behavior: Derives the project ID from the name. Creates the workspace directory at {config.workspace_dir}/{project_id}. Saves the project to the database.
Returns on success:
{
"created": <str: project_id>,
"name": <str>,
"workspace": <str: path>,
"auto_create_channels": <bool>,
}
Errors: None expected (directory creation errors will propagate as uncaught exceptions).
pause_project¶
Sets a project's status to PAUSED.
Parameters:
- project_id (required)
Returns on success:
Errors: - Project not found.
resume_project¶
Sets a project's status to ACTIVE.
Parameters:
- project_id (required)
Returns on success:
Errors: - Project not found.
edit_project¶
Updates one or more mutable fields on a project.
Parameters:
- project_id (required)
- name (optional): New display name.
- credit_weight (optional): New scheduler weight.
- max_concurrent_agents (optional): New concurrency limit.
At least one optional field must be supplied.
Returns on success:
Errors: - Project not found. - No updatable fields provided.
delete_project¶
Deletes a project and all associated database records (cascade). Fires _on_project_deleted callback if registered.
Parameters:
- project_id (required)
- archive_channels (optional, default False): Passed through to the caller in the response so the Discord layer can act on it. This handler does not archive channels itself.
Behavior: Checks whether any tasks are currently IN_PROGRESS and refuses deletion if so. Captures the project's Discord channel ID before deletion. After the database cascade completes, calls _on_project_deleted(project_id) if the callback is registered.
Returns on success:
{
"deleted": <str: project_id>,
"name": <str>,
"channel_ids": {"channel": <str>}, # present only if a channel was linked
"archive_channels": True, # present only if archive_channels=True was passed
}
Errors: - Project not found. - One or more tasks are IN_PROGRESS (caller must stop them first).
Channels¶
set_project_channel¶
Links an existing Discord channel to a project by storing the channel ID on the project record.
Parameters:
- project_id (required)
- channel_id (required): The Discord channel ID (as a string).
Returns on success:
Errors: - Project not found.
set_control_interface¶
Sets a project's channel by resolving a channel name to an ID, then delegating to set_project_channel.
Parameters:
- project_id (required; also accepted as project_name)
- channel_name (required): The Discord channel name (leading # is stripped).
- _resolved_channel_id (optional): If the caller (e.g. Discord slash command layer) has already resolved the channel ID, it may pass it here to skip the name-lookup step.
- guild_channels (optional): A list of {"id": <str/int>, "name": <str>} dicts representing all text channels in the guild. Required if _resolved_channel_id is not provided.
Behavior: Strips # from channel_name. Uses _resolved_channel_id if present; otherwise iterates guild_channels looking for a name match. If no match is found, returns an error. On success, delegates to _cmd_set_project_channel.
Returns on success: Same as set_project_channel.
Errors:
- project_id or channel_name not provided.
- Channel name not found in guild_channels.
- No guild context available to resolve the name (neither _resolved_channel_id nor guild_channels was supplied).
- Project not found (from delegated call).
get_project_channels¶
Returns the Discord channel ID configured for a project.
Parameters:
- project_id (required)
Returns on success:
Errors: - Project not found.
get_project_for_channel¶
Reverse lookup: given a Discord channel ID, returns the project linked to it.
Parameters:
- channel_id (required): The Discord channel ID (coerced to string).
Behavior: Scans all projects, comparing discord_channel_id to the given channel ID. Returns the first match. If no project is linked to this channel, returns the response with project_id: null.
Returns on success (match found):
Returns on success (no match):
Errors:
- channel_id not provided.
Tasks¶
list_tasks¶
Returns up to 200 tasks, optionally filtered.
Parameters:
- project_id (optional): Filter by project.
- status (optional): Filter by status string (e.g. "READY", "IN_PROGRESS").
Returns on success:
{
"tasks": [
{
"id": <str>,
"project_id": <str>,
"title": <str>,
"status": <str>,
"priority": <int>,
"assigned_agent": <str | None>,
},
...
],
"total": <int: total matching tasks before the 200-cap>,
}
Errors: None expected.
create_task¶
Creates a new task in READY status. If no project_id is given, the active project is used. Returns an error if no project can be resolved.
Parameters:
- title (required): Short task title.
- description (required): Full task description/prompt for the agent.
- project_id (optional, falls back to active project): Project to assign the task to.
- priority (optional, default: 100): Scheduling priority (lower value = higher priority).
- repo_id (optional): Associate the task with a specific repo.
- requires_approval (optional, default: False): If true, task moves to AWAITING_APPROVAL instead of COMPLETED when done.
Behavior: Generates a human-readable task ID using generate_task_id. Creates the task in READY status.
Returns on success:
{
"created": <str: task_id>,
"title": <str>,
"project_id": <str>,
"repo_id": <str>, # present only if repo_id was supplied
"requires_approval": True, # present only if requires_approval=True
}
Errors: None expected (DB errors propagate as uncaught exceptions).
get_task¶
Returns full details for a single task.
Parameters:
- task_id (required)
Returns on success:
{
"id": <str>,
"project_id": <str>,
"title": <str>,
"description": <str>,
"status": <str>,
"priority": <int>,
"assigned_agent": <str | None>,
"retry_count": <int>,
"max_retries": <int>,
"requires_approval": <bool>,
"pr_url": <str>, # present only if set
}
Errors: - Task not found.
edit_task¶
Updates one or more mutable fields on a task.
Parameters:
- task_id (required)
- title (optional)
- description (optional)
- priority (optional)
At least one optional field must be supplied.
Returns on success:
Errors: - Task not found. - No updatable fields provided.
stop_task¶
Stops a running task by delegating to orchestrator.stop_task.
Parameters:
- task_id (required)
Returns on success:
Errors: - Any error string returned by the orchestrator's stop logic.
restart_task¶
Resets a task back to READY status, clearing retry count and agent assignment.
Parameters:
- task_id (required)
Behavior: Refuses if the task is currently IN_PROGRESS (caller must stop it first). Transitions the task to READY, sets retry_count=0, clears assigned_agent_id.
Returns on success:
Errors: - Task not found. - Task is currently IN_PROGRESS.
delete_task¶
Deletes a task. If the task is IN_PROGRESS, it is stopped first.
Parameters:
- task_id (required)
Behavior: If the task is IN_PROGRESS, calls orchestrator.stop_task first and returns an error if stopping fails. After stopping (or if already stopped), deletes the task from the database.
Returns on success:
Errors: - Task not found. - Could not stop the running task before deleting.
approve_task¶
Approves a task that is in AWAITING_APPROVAL status, moving it to COMPLETED.
Parameters:
- task_id (required)
Behavior: Validates the task is in AWAITING_APPROVAL. Transitions to COMPLETED and logs a task_completed event.
Returns on success:
Errors: - Task not found. - Task is not in AWAITING_APPROVAL status.
set_task_status¶
Administratively force a task into any status (bypasses normal state-machine guards).
Parameters:
- task_id (required)
- status (required): Target status string (e.g. "READY", "BLOCKED", "COMPLETED").
Returns on success:
Errors: - Task not found.
skip_task¶
Skips a BLOCKED or FAILED task to unblock its dependency chain.
Parameters:
- task_id (required)
Behavior: Delegates to orchestrator.skip_task, which marks the task as skipped and promotes any downstream tasks that were waiting only on this one.
Returns on success:
{
"skipped": <str: task_id>,
"unblocked_count": <int>,
"unblocked": [{"id": <str>, "title": <str>}, ...],
}
Errors: - Any error string returned by the orchestrator's skip logic.
get_chain_health¶
Reports on dependency chain health — identifies downstream tasks that are stuck because an upstream task is BLOCKED.
Parameters:
- task_id (optional): Check a specific task's downstream chain.
- project_id (optional): Check all blocked tasks in a project.
If neither is provided, falls back to _active_project_id.
Behavior (task_id supplied):
- If the task is not BLOCKED, returns immediately with an empty stuck_downstream list.
- If BLOCKED, calls orchestrator._find_stuck_downstream(task_id) and returns the list of stuck tasks.
Behavior (project_id or active project): - Fetches all BLOCKED tasks for the project, checks each one for stuck downstream tasks, and aggregates the chains.
Returns on success (single task, not blocked):
{
"task_id": <str>,
"status": <str>,
"stuck_downstream": [],
"message": "Task is not blocked — no stuck chain.",
}
Returns on success (single task, blocked):
{
"task_id": <str>,
"title": <str>,
"status": <str>,
"stuck_downstream": [{"id": <str>, "title": <str>, "status": <str>}, ...],
"stuck_count": <int>,
}
Returns on success (project scope):
{
"project_id": <str | None>,
"stuck_chains": [
{
"blocked_task": {"id": <str>, "title": <str>},
"stuck_downstream": [{"id": <str>, "title": <str>}, ...],
"stuck_count": <int>,
},
...
],
"total_stuck_chains": <int>,
}
Errors:
- Task not found (when task_id is supplied).
get_task_result¶
Returns the stored result record for a task (the structured output saved when the agent finished).
Parameters:
- task_id (required)
Returns on success: The raw result dict as stored in the database.
Errors: - No result record found for the task.
get_task_diff¶
Returns the git diff for a task's branch relative to the repository's default branch.
Parameters:
- task_id (required)
Behavior: Looks up the task's repo_id and branch_name. Determines the checkout path: first tries the assigned agent's checkout_path, then falls back to repo.source_path. Calls git.get_diff(checkout_path, repo.default_branch).
Returns on success:
Errors: - Task not found. - Task has no associated repository. - Repository not found. - Task has no branch name. - Could not determine checkout path.
get_agent_error¶
Returns diagnostic information about a task's most recent failure.
Parameters:
- task_id (required)
Behavior: Fetches the task and its result. Classifies the error type using classify_error. Truncates the error message to 2000 characters and the agent summary to 1000 characters.
Returns on success:
{
"task_id": <str>,
"title": <str>,
"status": <str>,
"retries": "<retry_count> / <max_retries>",
"message": "No result recorded yet for this task", # if no result exists
# OR, if a result exists:
"result": <str: result value>,
"error_type": <str: classified error type>,
"error_message": <str | None: truncated to 2000 chars>,
"suggested_fix": <str: from classify_error>,
"agent_summary": <str>, # present only if summary exists, truncated to 1000 chars
}
Errors: - Task not found.
Agents¶
list_agents¶
Returns all registered agents.
Parameters: None.
Returns on success:
{
"agents": [
{
"id": <str>,
"name": <str>,
"type": <str: agent_type>,
"state": <str: agent state value>,
"current_task": <str | None: task_id>,
},
...
]
}
Errors: None expected.
create_agent¶
Registers a new agent.
Parameters:
- name (required): Human-readable agent name. Agent ID is derived by lowercasing and replacing spaces with hyphens.
- agent_type (optional, default "claude"): Agent type identifier.
- repo_id (optional): Associate the agent with a specific repo. Validated to exist.
Returns on success:
{
"created": <str: agent_id>,
"name": <str>,
"repo_id": <str>, # present only if repo_id was supplied
}
Errors:
- Repo not found (if repo_id was supplied).
Repos¶
add_repo¶
Adds a repository configuration to a project.
Parameters:
- project_id (required): Must exist.
- source (required): One of "clone", "link", or "init" (maps to RepoSourceType enum).
- url (required if source == "clone"): Git remote URL.
- path (required if source == "link"): Local filesystem path to an existing directory.
- default_branch (optional, default "main"): The repo's primary branch name.
- name (optional): Repo name. If not provided, derived from the URL (last path segment minus .git) or the path basename.
Behavior: Validates that for clone sources a URL is provided, and for link sources a valid directory path is provided. Derives the repo ID by lowercasing the name. Sets checkout_base_path from the workspace path (resolved via the workspaces table).
Returns on success:
Errors:
- Project not found.
- url missing for clone source.
- path missing for link source.
- path does not exist or is not a directory (for link source).
list_repos¶
Lists all repo configurations, optionally filtered by project.
Parameters:
- project_id (optional)
Returns on success:
{
"repos": [
{
"id": <str>,
"project_id": <str>,
"source_type": <str>,
"url": <str>,
"source_path": <str | None>,
"default_branch": <str>,
"checkout_base_path": <str | None>,
},
...
]
}
Errors: None expected.
Git High-Level Commands¶
These commands operate at the project level and are intended for human-facing use from Discord. They resolve the repo path via _resolve_repo_path and may include a "warning" field if IN_PROGRESS tasks exist.
create_branch¶
Creates and checks out a new branch in a project's repository.
Parameters:
- project_id (required)
- branch_name (required)
- repo_id (optional): Specific repo; otherwise the project's first repo is used.
Returns on success:
Errors:
- branch_name not provided.
- Repo path resolution failure (see _resolve_repo_path).
- Git error (e.g. branch already exists).
checkout_branch¶
Checks out an existing branch.
Parameters:
- project_id (required)
- branch_name (required)
- repo_id (optional)
Behavior: After successful checkout, calls _warn_if_in_progress and includes the warning in the result if any tasks are running.
Returns on success:
{
"project_id": <str>,
"repo_id": <str>,
"branch": <str: branch_name>,
"status": "checked_out",
"warning": <str>, # present only if IN_PROGRESS tasks exist
}
Errors:
- branch_name not provided.
- Repo path resolution failure.
- Git error (e.g. branch not found, dirty working tree).
commit_changes¶
Stages all changes and creates a commit.
Parameters:
- project_id (required)
- message (required): Commit message.
- repo_id (optional)
Behavior: Calls git.commit_all. If there is nothing to commit, returns status: "nothing_to_commit" (not an error). Calls _warn_if_in_progress on success.
Returns on success (committed):
{
"project_id": <str>,
"repo_id": <str>,
"commit_message": <str>,
"status": "committed",
"warning": <str>, # present only if IN_PROGRESS tasks exist
}
Returns on success (nothing to commit):
{
"project_id": <str>,
"repo_id": <str>,
"status": "nothing_to_commit",
"message": "No changes to commit",
}
Errors:
- message not provided.
- Repo path resolution failure.
- Git error.
push_branch¶
Pushes the current or specified branch to origin.
Parameters:
- project_id (required)
- branch_name (optional): Branch to push; defaults to the currently checked-out branch.
- repo_id (optional)
Returns on success:
Errors: - Repo path resolution failure. - Could not determine current branch. - Git error.
merge_branch¶
Merges a branch into the repository's default branch.
Parameters:
- project_id (required)
- branch_name (required)
- repo_id (optional)
Behavior: Uses repo.default_branch if available, otherwise "main". Calls _warn_if_in_progress regardless of merge outcome.
Returns on success (merged):
{
"project_id": <str>,
"repo_id": <str>,
"branch": <str>,
"target": <str: default_branch>,
"status": "merged",
"warning": <str>, # present only if IN_PROGRESS tasks exist
}
Returns on success (conflict):
{
"project_id": <str>,
"repo_id": <str>,
"branch": <str>,
"target": <str>,
"status": "conflict",
"message": "Merge conflict — merge was aborted",
"warning": <str>, # present only if IN_PROGRESS tasks exist
}
Errors:
- branch_name not provided.
- Repo path resolution failure.
- Git error.
Git Status Command¶
get_git_status¶
Reports git status across all repos for a project, or falls back to the project workspace if no repos are configured.
Parameters:
- project_id (required)
Behavior: For each repo, determines the checkout path (LINK uses source_path; CLONE uses checkout_base_path). Validates the path exists and is a git repo. Calls git.get_current_branch, git.get_status, and git.get_recent_commits(count=5). Does not use _resolve_repo_path; performs its own path resolution across all repos in the project.
Returns on success:
{
"project_id": <str>,
"project_name": <str>,
"repos": [
{
"repo_id": <str>,
"path": <str>,
"branch": <str>,
"status": <str: status output or "(clean)">,
"recent_commits": <str>,
},
...
],
}
Per-repo errors are embedded as {"repo_id": <str>, "error": <str>} entries rather than aborting the whole command.
Errors: - Project not found. - Project has no repos and no valid workspace path. - Project workspace is not a git repository.
Workspace Maintenance Commands¶
These commands operate across all workspaces for a project and are intended for the chat agent to diagnose and fix workspace issues. All git operations use asyncio.to_thread(subprocess.run, ...) or git._arun() to avoid blocking the event loop.
find_merge_conflict_workspaces¶
Scans all workspaces for a project to detect branches with merge conflicts against the default branch without modifying any working tree.
Parameters:
- project_id (optional): Falls back to _active_project_id.
Behavior: For each workspace, fetches from origin, then iterates all remote branches. For each branch (excluding the default branch, HEAD, and dependabot/*), runs git merge-base to find the common ancestor, then git merge-tree to simulate a three-way merge. If the merge-tree output contains conflict markers (+<<<<<<<), the branch is flagged as conflicting. Also checks for active working tree conflicts via git status --porcelain (looking for UU, AA, DD status codes).
All git operations are run via asyncio.to_thread(subprocess.run, ...) since they are one-off diagnostic commands not covered by GitManager methods.
Returns on success:
{
"project_id": <str>,
"workspaces_scanned": <int>,
"workspaces_with_conflicts": <int>,
"conflicts": [
{
"workspace_id": <str>,
"workspace_name": <str>,
"workspace_path": <str>,
"current_branch": <str>,
"locked_by_task_id": <str | None>,
"locked_by_agent_id": <str | None>,
"has_working_tree_conflict": <bool>,
"branch_conflicts": [
{
"branch": <str>,
"task_id": <str>,
"conflicting_files": [<str>, ...],
"commits_behind_main": <str>,
},
...
],
},
...
],
}
Errors:
- project_id not provided and no active project set.
- Project not found.
- No workspaces found for the project.
sync_workspaces¶
Synchronizes all workspaces for a project to the latest main branch.
Parameters:
- project_id (optional): Falls back to _active_project_id.
- skip_locked (optional, default True): Skip workspaces locked by an agent.
Behavior: Delegates to _sync_single_workspace for each workspace. Uses a mix of git._arun() for standard git operations and asyncio.to_thread(subprocess.run, ...) for status checks.
Per-workspace sync logic:
1. Validates the workspace is a valid git repo directory.
2. Skips workspaces locked by an agent (unless skip_locked=False).
3. Fetches latest from origin via git._arun(["fetch", "origin", "--prune"]).
4. Gets current branch via git.aget_current_branch().
5. Checks for uncommitted changes and active merge conflicts via git status --porcelain.
6. If on the default branch: stashes uncommitted changes if present, then hard-resets to origin/<default_branch>.
7. If on a feature branch:
- Auto-commits uncommitted changes via git.acommit_all().
- Pushes the branch to origin via git.apush_branch(force_with_lease=True).
- Updates the local default branch reference (in worktrees, uses git update-ref; in normal repos, checks out default, hard-resets, then returns to the feature branch).
- Attempts to rebase the feature branch onto origin/<default_branch>. If rebase conflicts, aborts and leaves the branch as-is.
Returns on success:
{
"project_id": <str>,
"default_branch": <str>,
"total_workspaces": <int>,
"synced": <int>,
"skipped": <int>,
"errors": <int>,
"workspaces": [<per-workspace result dicts>, ...],
}
Per-workspace result dicts include workspace_id, workspace_name, workspace_path, status ("synced", "skipped", "conflict", or "error"), and additional fields depending on the action taken.
Errors:
- project_id not provided and no active project set.
- Project not found.
- No workspaces found for the project.
Git Low-Level Commands¶
These commands use _resolve_repo_path for path lookup. The first group (git_commit, git_push, git_create_branch, git_merge, git_create_pr, git_changed_files) use repo_id as the primary key and require it. The second group (git_log, git_branch, git_checkout, git_diff) use project_id as the primary key with repo_id as an optional filter.
git_commit¶
Stage all changes and commit in a repository.
Parameters:
- repo_id (required)
- message (required): Commit message.
- project_id (optional)
Returns on success (committed):
Returns on success (nothing to commit):
Errors: - Repo path resolution failure. - Git error.
git_push¶
Push a branch to the remote origin.
Parameters:
- repo_id (required)
- branch (optional): Branch to push; defaults to current branch.
- project_id (optional)
Returns on success:
Errors: - Repo path resolution failure. - Could not determine current branch. - Git error.
git_create_branch¶
Create and switch to a new branch.
Parameters:
- repo_id (required)
- branch_name (required)
- project_id (optional)
Returns on success:
Errors: - Repo path resolution failure. - Git error.
git_merge¶
Merge a branch into the default branch.
Parameters:
- repo_id (required)
- branch_name (required): The branch to merge.
- default_branch (optional): Target branch; falls back to repo.default_branch then "main".
- project_id (optional)
Returns on success (merged):
Returns on success (conflict):
{
"repo_id": <str>,
"merged": False,
"into": <str>,
"message": "Merge conflict — merge of '<branch>' into '<default_branch>' was aborted",
}
Errors: - Repo path resolution failure. - Git error.
git_create_pr¶
Create a GitHub pull request using the gh CLI.
Parameters:
- repo_id (required)
- title (required): PR title.
- body (optional, default ""): PR description body.
- branch (optional): Source branch; defaults to current branch.
- base (optional): Target branch; defaults to repo.default_branch then "main".
- project_id (optional)
Returns on success:
Errors:
- Repo path resolution failure.
- Could not determine current branch.
- Git error (e.g. gh CLI not installed, not authenticated).
git_changed_files¶
List files changed compared to a base branch.
Parameters:
- repo_id (required)
- base_branch (optional): Comparison base; defaults to repo.default_branch then "main".
- project_id (optional)
Returns on success:
Errors: - Repo path resolution failure.
git_log¶
Show recent commit history for a repository.
Parameters:
- project_id (required)
- count (optional, default 10): Number of commits to return.
- repo_id (optional)
Returns on success:
{
"project_id": <str>,
"repo_id": <str>,
"branch": <str>,
"log": <str: formatted commit log or "(no commits)">,
}
Errors: - Repo path resolution failure.
git_branch¶
List branches or create a new branch.
Parameters:
- project_id (required)
- name (optional): If provided, a new branch is created and checked out; otherwise branches are listed.
- repo_id (optional)
Returns on success (list):
Returns on success (create):
{
"project_id": <str>,
"created": <str: branch_name>,
"message": "Created and switched to branch '<name>'",
}
Errors: - Repo path resolution failure. - Git error (branch creation only).
git_checkout¶
Switch to an existing branch.
Parameters:
- project_id (required)
- branch (required): Name of the branch to check out.
- repo_id (optional)
Returns on success:
{
"project_id": <str>,
"old_branch": <str>,
"new_branch": <str>,
"message": "Switched from '<old>' to '<new>'",
}
Errors: - Repo path resolution failure. - Git error.
git_diff¶
Show a diff of the working tree or against a base branch.
Parameters:
- project_id (required)
- base_branch (optional): If provided, runs git.get_diff(path, base_branch); otherwise shows the working tree diff (unstaged changes via git diff).
- repo_id (optional)
Returns on success:
{
"project_id": <str>,
"repo_id": <str>,
"base_branch": <str: base or "(working tree)">,
"diff": <str: diff text or "(no changes)">,
}
Errors: - Repo path resolution failure. - Git error.
Hooks¶
create_hook¶
Creates a new hook for a project.
Parameters:
- project_id (required): Must exist.
- name (required): Human-readable hook name. Hook ID is derived by lowercasing and replacing spaces with hyphens.
- trigger (required): Dict describing when the hook fires (serialized as JSON).
- prompt_template (required): Jinja/string template for the LLM prompt.
- context_steps (optional, default []): List of context-gathering steps (serialized as JSON).
- cooldown_seconds (optional, default 3600): Minimum seconds between firings.
- llm_config (optional): Dict of LLM configuration overrides (serialized as JSON).
Returns on success:
Errors: - Project not found.
list_hooks¶
Lists all hooks, optionally filtered by project.
Parameters:
- project_id (optional)
Returns on success:
{
"hooks": [
{
"id": <str>,
"project_id": <str>,
"name": <str>,
"enabled": <bool>,
"trigger": <dict: deserialized from JSON>,
"cooldown_seconds": <int>,
},
...
]
}
Errors: None expected.
edit_hook¶
Updates one or more fields on an existing hook.
Parameters:
- hook_id (required)
- enabled (optional): Boolean.
- trigger (optional): Dict (serialized to JSON).
- context_steps (optional): List (serialized to JSON).
- prompt_template (optional): String.
- cooldown_seconds (optional): Integer.
- llm_config (optional): Dict (serialized to JSON).
At least one optional field must be supplied.
Returns on success:
Errors: - Hook not found. - No updatable fields provided.
delete_hook¶
Deletes a hook.
Parameters:
- hook_id (required)
Returns on success:
Errors: - Hook not found.
list_hook_runs¶
Returns recent execution records for a hook.
Parameters:
- hook_id (required)
- limit (optional, default 10): Maximum number of run records to return.
Returns on success:
{
"hook_id": <str>,
"hook_name": <str>,
"runs": [
{
"id": <str>,
"trigger_reason": <str>,
"status": <str>,
"tokens_used": <int>,
"skipped_reason": <str | None>,
"started_at": <str | float>,
"completed_at": <str | float | None>,
},
...
],
}
Errors: - Hook not found.
fire_hook¶
Manually fires a hook immediately (bypasses cooldown).
Parameters:
- hook_id (required)
Behavior: Delegates to orchestrator.hooks.fire_hook(hook_id). The hook runs asynchronously.
Returns on success:
Errors:
- Hook engine is not enabled (orchestrator.hooks is None).
- Hook not found (raised as ValueError by the hooks engine).
Notes¶
Notes are stored as Markdown files in {workspace_path}/notes/, where workspace_path is resolved from the workspaces table via db.get_project_workspace_path(). If the project has no workspaces, note commands return an error. Filenames are derived from the title using git.slugify.
list_notes¶
Lists all notes for a project.
Parameters:
- project_id (required)
Behavior: Reads filenames from the notes/ directory (.md files only, sorted alphabetically). For each file, reads the first line to extract a title (if it starts with #); otherwise derives the title from the filename.
Returns on success:
{
"project_id": <str>,
"notes": [
{
"name": <str: filename>,
"title": <str>,
"size_bytes": <int>,
"modified": <float: mtime>,
"path": <str: absolute path>,
},
...
],
}
If the notes/ directory does not exist, returns an empty notes list (not an error).
Errors: - Project not found.
write_note¶
Creates or overwrites a note.
Parameters:
- project_id (required)
- title (required): Note title. Slugified for the filename.
- content (required): Full Markdown content to write.
Behavior: Creates the notes/ directory if it does not exist. Slugifies the title to form the filename. Writes the content (overwriting any existing file with the same slug).
Returns on success:
Errors: - Project not found. - Title produces an empty slug after slugification.
delete_note¶
Deletes a note by title.
Parameters:
- project_id (required)
- title (required): Title of the note to delete. Slugified to find the file.
Returns on success:
Errors: - Project not found. - Note not found (file does not exist).
read_note¶
Reads a note's contents by title without needing the full path.
Parameters:
- project_id (required)
- title (required): Note title. Slugified to resolve the filename.
Behavior: Resolves the file path as {workspace}/notes/{slugify(title)}.md and reads the full content.
Returns on success:
{
"content": <str: full file content>,
"title": <str>,
"path": <str: absolute file path>,
"size_bytes": <int>,
}
Errors: - Project not found. - Note not found (file does not exist).
append_note¶
Appends content to an existing note, or creates a new note if one does not exist.
Parameters:
- project_id (required)
- title (required): Note title. Slugified for the filename.
- content (required): Content to append.
Behavior: Resolves the file path as {workspace}/notes/{slugify(title)}.md. If the file exists, appends \n\n followed by the new content. If the file does not exist, creates it with # {title}\n\n{content}. Creates the notes/ directory if it does not exist.
Returns on success:
{
"path": <str: absolute file path>,
"title": <str>,
"status": "appended" | "created",
"size_bytes": <int>,
}
Errors: - Project not found. - Title produces an empty slug after slugification.
compare_specs_notes¶
Lists specs and notes files for gap analysis. Returns raw file listings — no LLM call is made.
Parameters:
- project_id (required)
- specs_path (optional): Override for the specs directory path. If not provided, the command checks for a specs/ directory in the project's repo first, then falls back to {workspace}/specs/.
Behavior: Resolves the specs directory (repo specs/ first, then {workspace}/specs/). Lists all .md files in both the specs directory and the {workspace}/notes/ directory, returning titles and sizes for each.
Returns on success:
{
"specs": [{"name": <str>, "title": <str>, "size_bytes": <int>}, ...],
"notes": [{"name": <str>, "title": <str>, "size_bytes": <int>}, ...],
"specs_path": <str: absolute path to specs dir>,
"notes_path": <str: absolute path to notes dir>,
"project_id": <str>,
}
Errors: - Project not found.
System¶
get_recent_events¶
Returns recent system events from the event log.
Parameters:
- limit (optional, default 10): Number of events to return.
Returns on success:
Errors: None expected.
get_token_usage¶
Returns token usage statistics, scoped to a task, project, or the entire system.
Parameters:
- task_id (optional): If provided, returns per-agent token breakdown for this task.
- project_id (optional): If provided (and task_id not given), returns per-task/agent breakdown for this project.
- Neither: Returns per-project totals system-wide.
Returns on success (task scope):
{
"task_id": <str>,
"breakdown": [{"agent_id": <str>, "tokens": <int>, "entries": <int>}, ...],
"total": <int>,
}
Returns on success (project scope):
{
"project_id": <str>,
"breakdown": [{"task_id": <str>, "agent_id": <str>, "tokens": <int>}, ...],
"total": <int>,
}
Returns on success (system scope):
Errors: None expected.
set_active_project¶
Sets or clears the _active_project_id on the handler, which is used as a fallback scope for certain commands (e.g. get_chain_health).
Parameters:
- project_id (optional): If not provided or empty, the active project is cleared.
Returns on success (set):
Returns on success (cleared):
Errors:
- Project not found (when project_id is provided).
orchestrator_control¶
Pauses, resumes, or checks the status of the orchestrator loop.
Parameters:
- action (required): One of "pause", "resume", "status".
Returns on success (pause):
Returns on success (resume):
Returns on success (status):
Errors: None expected (unrecognized action falls through to the status case).
restart_daemon¶
Logs a restart notification to the notification channel, then sends SIGTERM to the current process, causing the daemon to shut down (and presumably restart via a process manager). Sets orchestrator._restart_requested = True before sending the signal.
Parameters:
- reason (optional, default "No reason provided"): Human-readable reason for the restart. Logged to the notification channel as "🔄 **Daemon restart initiated** — {reason}".
Returns on success:
Errors: None expected.
update_and_restart¶
Pulls the latest source from git and restarts the daemon. Determines the repo root from the source file location. Runs git pull --ff-only followed by pip install -e . to pick up dependency changes. Both commands are run in a thread via asyncio.to_thread(subprocess.run, ...) to avoid blocking the event loop. On success, logs a notification and triggers a restart via SIGTERM.
Parameters:
- reason (optional, default "No reason provided"): Human-readable reason for the update.
Returns on success:
{"status": "updating", "message": "Update pulled and daemon restart initiated", "pull_output": <str>, "reason": <str>}
Errors:
- git pull failed (non-zero exit code).
- pip install failed (non-zero exit code).
read_file¶
Reads a file from within an allowed directory. Intended for the chat agent, not Discord slash commands.
Parameters:
- path (required): File path. If not absolute, it is joined with config.workspace_dir.
- max_lines (optional, default 200): Maximum lines to return. If the file is longer, a truncation notice is appended.
Behavior: Resolves the path and validates it via _validate_path. Reads up to max_lines lines. Returns an error for binary files (UnicodeDecodeError).
Returns on success:
Errors: - Path is outside allowed directories. - File not found. - Binary file (cannot display).
run_command¶
Executes a shell command in a validated working directory. Intended for the chat agent.
Parameters:
- command (required): Shell command string (executed via shell=True).
- working_dir (required): Directory to run the command in. If not an absolute path, the handler first tries to look it up as a project ID; if that fails, it joins with config.workspace_dir.
- timeout (optional, default 30, max 120): Execution timeout in seconds.
Behavior: Validates the working directory via _validate_path. Runs the command in a thread via asyncio.to_thread(subprocess.run, command, shell=True, ...) to avoid blocking the event loop. Stdout is truncated to 4000 characters; stderr to 2000 characters.
Returns on success:
Errors: - Working directory is outside allowed directories. - Directory not found. - Command timed out.
search_files¶
Searches for patterns in files within a validated directory. Intended for the chat agent.
Parameters:
- pattern (required): Search pattern (regex for grep mode; glob for find mode).
- path (required): Directory to search. If not absolute, joined with config.workspace_dir.
- mode (optional, default "grep"): Either "grep" (recursive regex search, up to 50 matches) or "find" (filename glob search).
Behavior: Validates the directory via _validate_path. Runs the search command in a thread via asyncio.to_thread(subprocess.run, ...). In grep mode: grep -rn --include=* -m 50 <pattern> <path>. In find mode: find <path> -name <pattern> -type f. Output is truncated to 4000 characters.
Returns on success:
Errors: - Path is outside allowed directories. - Directory not found. - Search timed out.
list_directory¶
Lists files and directories at a given path within a project workspace. Used by both the chat agent and the /browse Discord command.
Parameters:
- project_id (optional if active project is set): Project whose workspace to browse.
- workspace (optional): Workspace name or ID. If omitted, the first workspace for the project is used. Looked up by name first, then by ID.
- path (optional, default ""): Relative path within the workspace to list. Empty string lists the workspace root.
Behavior: Resolves the workspace path to an absolute path via os.path.realpath() to prevent CWD-relative resolution issues (e.g., if a workspace path was stored as a relative path, it could otherwise resolve relative to the bot's working directory). Joins the relative path with the workspace root, validates it via _validate_path, then lists the directory contents. Entries are sorted alphabetically and separated into directories and files (with sizes).
Returns on success:
{
"project_id": <str>,
"path": <str: relative path or "/">,
"workspace_path": <str: resolved absolute workspace root>,
"workspace_name": <str>,
"directories": [<str>, ...],
"files": [{"name": <str>, "size": <int>}, ...],
}
Errors:
- project_id is required (no active project set).
- Workspace not found for project.
- Project has no workspaces.
- Access denied: path is outside allowed directories.
- Directory not found.
- Permission denied.
5. Path Validation (_validate_path)¶
This method is a security gate that ensures file operations cannot escape designated directories.
Logic:
1. Resolves the given path to its canonical realpath (resolves symlinks).
2. Resolves config.workspace_dir to its canonical realpath.
3. If the resolved path is within (starts with {workspace_real}/) or equal to workspace_real, the canonical path is returned (allowed).
4. Otherwise, fetches all repos from the database. For each repo that has a source_path, resolves that source_path to its canonical realpath. If the given path is within or equal to any repo's source_path, the canonical path is returned (allowed).
5. If none of the above match, returns None (denied).
The caller is responsible for checking whether None was returned and returning an access-denied error to the user.
6. Repo Path Resolution (_resolve_repo_path)¶
Returns a 3-tuple: (checkout_path, repo_config, error_dict). On success, error_dict is None. On failure, checkout_path is None.
Logic:
- Reads
project_idandrepo_idfromargs. - If neither is provided, returns an error.
- If
project_idis provided, fetches and validates the project exists.repo_idalone (withoutproject_id) is also a valid input — the repo is looked up directly, which keeps older repo-id-only commands working. - Repo resolution:
- If
repo_idis provided, fetches that specific repo (error if not found). - If only
project_idis provided, fetches the project's repos and takes the first one (orNoneif none exist). - Path determination:
- If a repo was found:
LINKsource type: usesrepo.source_path.CLONEorINITsource type: usesrepo.checkout_base_path.- If neither path is set: error.
- If no repo was found: returns an error telling the user to add workspaces via
/add-workspace. - Validates that the determined path exists as a directory (error if not).
- Calls
git.validate_checkout(checkout_path)to confirm it is a git repository (error if not). - Returns
(checkout_path, repo, None).
Summary of error conditions:
- Neither project_id nor repo_id provided.
- Project not found.
- Repo not found (when repo_id specified).
- Repo has no usable path configured.
- No repo found and no project context.
- Project has no repos and no valid workspace.
- Path does not exist on disk.
- Path is not a valid git repository.
7. In-Progress Warning (_warn_if_in_progress)¶
Queries the database for any tasks with status IN_PROGRESS for the given project. If any are found, returns a warning string of the form:
⚠️ {n} task(s) currently IN_PROGRESS for this project — this operation may disrupt running agent(s).
If no tasks are in progress, returns None.
This method is called by the high-level git commands checkout_branch, commit_changes, and merge_branch after a successful git operation. The warning is included in the response dict under the "warning" key when present. It is never a blocking error — the git operation proceeds regardless.