f1da9644f4
* feat: add MCP mode pipeline for config vs task tool registration
Thread McpMode field through the entire instance creation chain:
CreateInstanceRequest → InstanceOverrides → InstanceConfig → MCP Server.
This allows config-mode MCP sessions to register a different set of
tools than the default task-mode sessions.
* feat: add config-mode MCP tools and agentctl handlers
Register config-mode tools (workflow steps, agents, MCP config, tasks)
when MCP server mode is "config". Add 16 new WS action constants and
agentctl-side handlers that forward tool calls to the backend via the
existing BackendClient channel.
* feat: add backend MCP handlers for config-mode operations
Add backend WebSocket handlers for workflow step CRUD, agent CRUD,
MCP config get/update, and task move/delete/archive/state operations.
Wire config dependencies via SetConfigDeps pattern to avoid breaking
the existing NewHandlers constructor.
* feat: add config-mode system prompt and unit tests
Add ConfigContext system prompt listing all config-mode MCP tools.
Add unit tests for mode-based tool registration, agentctl config
handler forwarding, backend handler validation, and system prompt
content.
* test: add exhaustive E2E tests for config-mode MCP handlers
38 end-to-end tests covering the full handler → service → DB pipeline
for all 16 config-mode operations: workflow step CRUD, agent CRUD,
agent profiles, MCP config, and task management (move, delete, archive,
update state). Includes handler registration verification and two
full pipeline integration tests.
* fix: remove E2E test that depends on agent CLIs installed in CI
TestE2E_Agent_CreateMultipleAndList tried to create claude-code and
codex agents, which aren't available in CI. Agents are unique by type
ID so creating duplicates also fails. Removed the test since
CreateAndList already validates the create+list flow.
* test: remove Go E2E tests for config-mode handlers
Replacing with Playwright E2E tests.
* test: add Playwright E2E tests for config-mode features
13 tests covering workflow steps, task operations, agent/profile
settings pages, MCP config, and workflow settings round-trips.
* feat: add dynamic MCP mode reconfiguration for agentctl instances
Add SetMode() to the MCP server that clears and re-registers tools for
the new mode. Expose it via PUT /api/v1/mcp/mode on per-instance servers,
a SetMcpMode client method, and a lifecycle Manager.SetMcpMode wrapper.
This enables switching from task to config MCP tools on workspaces that
were prepared before plan_mode was set on the session.
* feat: wire McpMode through executor launch and resume pipelines
Add McpMode to LaunchOptions so callers can set it at launch time.
StartTask and StartTaskWithSession now pass McpMode="config" when
plan mode is active. startAgentOnExistingWorkspace reconfigures the
MCP server via SetMcpMode before starting the agent subprocess.
The AgentManagerClient interface gains SetMcpMode; the lifecycle
adapter, executor, and all mock implementations are updated.
* fix: pass plan_mode from workflow step when auto-starting moved tasks
handleTaskMovedNoSession was hardcoding planMode=false when calling
StartTask. Now it loads the target step and checks for enable_plan_mode
in on_enter actions, passing the correct value so config-mode MCP tools
are registered for the agent.
* test: fix config-mode E2E tests and add SetMcpMode mock stubs
- Add SetMcpMode stubs to all AgentManager mock implementations
- Fix strict mode violations with { exact: true } on tool name assertions
- Expand collapsed tool call groups before asserting individual names
- Remove temporary diagnostic code
* fix: address CodeRabbit review comments
- Add servers parameter to update_mcp_config tool schema
- Add mutex protection to SetMode for thread safety
- Create session before building prompt so config context has session ID
- Fix inconsistent ask_user_question tool name reference (use _kandev suffix)
* refactor: use canonical ws.ActionMCP constants and handle marshal errors
- Import ActionMCP* constants from pkg/websocket instead of duplicating
them locally in config_handlers.go and handlers.go
- Handle json.MarshalIndent error in forwardToBackend instead of ignoring it
* fix: extract McpModeConfig constant and fix Prettier formatting
- Add McpModeConfig constant to avoid repeated "config" string (goconst)
- Fix Prettier formatting in config-management E2E spec
* fix: validate task state before update and strengthen sysprompt tests
- Add TaskState validation in handleUpdateTaskState to return proper
validation error instead of internal error for invalid states
- Strengthen format-contract assertions (exact %s counts, no %! markers)
- Extract repeated test data into constants per project guidelines
* refactor: decouple config MCP mode from plan_mode, use session metadata
Replace plan_mode-based config MCP activation with config_mode in
session metadata. Add isConfigModeSession() helper used across
executor_execute, executor_interaction, and executor_resume.
Propagate task.Metadata to session.Metadata in PrepareSession so
config_mode flows from task creation through to the agent launch.
* refactor: remove task tools from config MCP mode and update sysprompt
Remove task management tools (list_tasks, move_task, delete_task,
archive_task) from ModeConfig registration — they remain available in
ModeTask. Update ConfigContext system prompt to remove task tool docs
and add example requests section. Update MCP server tests for new tool
count (18→14) and remove deleted task handler tests.
* feat: add config-chat endpoint and workspace config agent profile
Add default_config_agent_profile_id column to workspaces with SQLite
migration. Add POST /workspaces/:id/config-chat endpoint that creates
an ephemeral task with config_mode metadata and starts a session with
config MCP tools. Extract resolveConfigChatDefaults helper to stay
within lint complexity limits.
* feat: add config chat UI with workspace settings integration
Add config chat modal component and hook for starting config sessions.
Add configChat state slice with open/close actions. Add workspace API
client for config-chat endpoint. Add configuration agent profile
selector to workspace settings form. Add config chat button to
settings layout sidebar.
* test: rewrite config-mode E2E tests to use metadata and session_id
Rewrite config-management E2E tests to use createTaskWithAgent with
config_mode metadata instead of a custom fetch helper. Fix navigation
to use task.session_id instead of task.id. Add metadata parameter
support to ApiClient.createTaskWithAgent.
* fix: use floating FAB for config chat and pass default agent profile
Move config chat button from header to a floating action button in the
bottom-right corner with a sparkles icon. Pass the workspace's default
agent profile ID when opening the config chat to avoid "no agent
profile configured" error. Add default_config_agent_profile_id to
workspace slice type.
* feat: async agent launch for config-chat and extract helpers
The config-chat endpoint now launches the agent asynchronously when a
prompt is provided, matching the start_agent flow used by task creation.
Extract deleteTaskOnError and launchConfigChatAgent helpers. Remove
LaunchWorkspace from prepare step to avoid blocking.
* feat: first-interaction agent profile selection for config chat
Open the config chat modal immediately on FAB click. If no default
config agent profile is set, the empty state now shows a profile
selector before starting. The selected profile is saved as the
workspace default_config_agent_profile_id for future sessions.
* test: fix config E2E tests — remove duplicate method, use real tools
Remove duplicate startConfigChat method in ApiClient that overrode the
correct 3-param signature. Replace invalid create_agent test (requires
registered agent type) with update_agent test. Remove fragile tool calls
group UI assertion from multi-tool test.
* fix: resolve 'no active session' error in config chat
Add openConfigChatModal action that only sets isOpen and workspaceId
without touching sessionId. Previously, open() called openConfigChat
with empty sessionId which was truthy, causing QuickChatContent to
render with an invalid session.
* feat: add minimal toolbar mode for config chat input
Thread minimalToolbar prop through ChatInputArea → ChatInputContainer →
ChatInputBody → ChatInputToolbar → QuickChatContent. When enabled,
renders only the submit/cancel button, hiding plan toggle, MCP
indicator, context popover, model selector, and other controls.
* feat: replace config chat dialog with floating popover panel
Replace Dialog with Popover from @kandev/ui so the config chat floats
up from the FAB button. Reduce FAB size to h-12/w-12 with larger h-7/w-7
icon. Rename tooltip to "Configuration Chat". Remove standalone
ConfigChatFAB component from settings layout.
* feat: filter config chat tasks from quick chat listing
Add exclude_config query parameter to ListTasksByWorkspace that filters
out tasks with metadata.config_mode=true. Apply it in the quick chat
sessions API call to prevent config chat tasks from appearing as tabs.
* refactor: move config agent profile setting to agents page
Remove the Configuration Agent Profile selector from workspace settings
and add a dedicated ConfigChatAgentSection to the agents settings page
with auto-save on change.
* feat: redesign config chat empty state with inline text input
Replace clickable template buttons with subtle suggestion hints that
fill the textarea on click and disappear once the user starts typing.
Add a persistent Textarea + send button at the bottom so users can
type directly without clicking templates. This prevents the "no active
session" error since QuickChatContent only renders after startSession
succeeds.
* fix: config chat UX polish — non-clickable suggestions, profile persistence
- Make suggestion prompts purely visual (non-interactive text hints)
- Update workspace store after saving config agent profile so the
profile selector doesn't reappear on subsequent opens
- Use useAppStoreApi for store access in callbacks
* fix: add inline error display and prevent accidental popover dismiss
- Show API errors inline below the input instead of via toast (which
can be hidden behind the popover)
- Prevent popover from closing on outside click via onInteractOutside,
so the user doesn't lose their typed input or active session
- Surface error state from useConfigChat hook to the empty state UI
* fix: use config-specific placeholder in config chat input
Add placeholderOverride prop through ChatInputArea and QuickChatContent
so the config chat shows "Ask anything about your configuration..."
instead of the default "Continue working on the task..." placeholder.
* feat: add default_config_agent_profile_id to workspace DTO and API
Wire the field through WorkspaceDTO, FromWorkspace(), HTTP/WS request
structs, service layer, and public API types so the config chat default
profile persists across page reloads and SSR hydration.
* feat: add config chat store actions and prompt delivery via WS
Add configChat slice actions (open, close, rename, tab management),
pendingPrompt state in useConfigChat hook, and initialPrompt support
in QuickChatContent so prompts are delivered via WS after subscription
is established rather than via HTTP (avoiding race condition).
* fix: resolve dual store issue and message fetch race condition
Replace StateProvider with StateHydrator in settings layout to prevent
a shadow Zustand store that intercepted WS events. Fix message fetch
race condition by merging WS-delivered messages with fetch results
instead of replacing them.
* feat: make config chat global with command panel integration
Add ConfigChatProvider at root layout so the popover is available on all
pages. FAB button only visible on settings pages; hidden anchor on other
pages for popover positioning. Add "Configuration Chat" command to the
command panel (Cmd+K). Add close button and header bar to the popover.
* test: add E2E tests for config chat popover and command panel
Add tests for opening config chat via FAB on settings page (with full
agent response flow) and opening via command panel on kanban page.
Fix api-client endpoint and add workspace update fields for E2E setup.
* fix: compress props destructuring to satisfy max-lines-per-function lint
Rebase merged both sides' additions into ChatInputEditorArea and
ChatInputToolbar, pushing them over the 100-line function limit.
Use object parameter with compact destructuring to reduce line count.
* fix: register config task tools and add exhaustive E2E coverage
Config-mode MCP task tools (list_tasks, move_task, delete_task,
archive_task) were defined but never registered — registerConfigTaskTools()
was never called in registerTools(). Fixed the registration and expanded
E2E tests from 5 to 15, covering all testable config-mode MCP tools
(workflow, agent, MCP config, and task management) with API-side
verification of side effects.
* feat: replace create/delete agent with create/delete agent profile in config mode
Agent creation/deletion via MCP isn't practical since agents map to
registered types. Replace with create_agent_profile and
delete_agent_profile tools that manage the per-agent configuration
presets users actually need to work with. Wires full chain: agentctl
MCP tools → WS actions → backend handlers → controller layer.
* chore: fix Prettier formatting
* fix: compress props destructuring to satisfy max-lines-per-function lint
* test: update MCP tests for agent profile tools and correct config tool count
* fix: address CodeRabbit review — normalize MCP mode, set default, consolidate tests
* fix: address CodeRabbit review — error leaks, search filter bug, stale prompt, sysprompt tools
* feat: expose all workflow step fields in config-mode MCP tools
* feat: add workflow CRUD to config-mode MCP tools
* fix: split workflow CRUD E2E test into separate tests
* fix: prevent cascade deletion of config session in delete workflow E2E test
The config-chat endpoint picks the newest workflow (ORDER BY created_at
DESC) for the ephemeral task. When the test created a deletable workflow
right before starting the config session, the session's own task landed
under that workflow. Deleting the workflow then cascaded and wiped the
running session's task, causing the MCP tool call to hang.
Add a "Config Anchor" bumper workflow so the config session uses it
instead of the deletable one.
* feat: add executor management to config-mode MCP tools
Add 8 new MCP tools for executor and executor profile CRUD in config
mode: list/create/update/delete for both executors and profiles.
Wired through the full stack (WS actions, backend handlers, agentctl
tools, sysprompt) with unit tests and E2E coverage.
* fix: auto-focus config chat input and rename popover title
* fix: remove executor CRUD MCP tools, keep executor profile management
Executors are pre-defined system entities (local, worktree, docker,
sprites). Only executor profiles should be managed via config chat.
Removes create_executor, update_executor, delete_executor MCP tools
while keeping list_executors and all executor profile tools.
* fix: add realtime UI sync for workflow steps and agent profiles via MCP
Workflow step and agent profile changes made through the config chat
MCP tools were not broadcasting events to the frontend. Executor
profile events worked because they went through the task service which
has event bus integration.
- Add WorkflowStep and AgentProfile event types to the event bus
- Add WS notification action constants for workflow step events
- Publish events from MCP handlers after step/profile CRUD operations
- Register gateway subscriptions for the new event types
- Add frontend WS handlers for workflow.step.created/updated/deleted
* fix: reuse existing config chat session when opened from command panel
The command panel always called openConfigChatModal() which reset the
session state, causing the profile picker to show again. Now it checks
for an existing active session first, matching the FAB behavior.
* fix: suppress FAB tooltip while config chat popover is open
When closing the popover via the X button, the mouse ends up over the
FAB area and the tooltip re-appears. Force the tooltip closed while
the popover is open so it cannot re-trigger on close.
* fix: thread default_config_agent_profile_id through all workspace data paths
The field was defined on the workspace type but never populated in:
- SSR mapper (page.tsx) — missing from initial hydration
- WS workspace handlers — wiped on workspace.created/updated events
- Backend publishWorkspaceEvent — not included in event data
This caused the config chat to always show the profile picker when
opened from the command panel, since the saved default was lost.
* fix: address PR review comments — workspace sync, type fixes, missing tool doc
- Sync configChat.workspaceId when promoting next tab in closeConfigChatSession
- Add update_task_state_kandev to ConfigContext system prompt
- Fix workflow step WS event types (workflow.step.* instead of step.*)
- Fix const reassignment in E2E tests (const → let)
* refactor: clean up dead code and register missing MCP tools
- Register update_task_state, delete_workflow_step, and
reorder_workflow_steps as MCP tools in agentctl config mode
- Remove dead handleCreateAgent/handleDeleteAgent WS handlers
and their ActionMCP constants (only profiles are managed)
- Rename openConfigChatModal to startNewConfigChat across frontend
- Update sysprompt, tests, and tool counts
* fix: remove extra next.config.ts machine
* fix: address CI failures — prettier formatting and funlen split
* fix: address coderabbit review comments across config-mode stack
- Include prompt field in workflow step broadcast payloads
- Align sysprompt task-state values with actual backend enum
- Return error instead of continuing when MCP mode switch fails
- Validate mode values in agentctl set-mcp-mode endpoint
- Use errors.Is() for sentinel error comparison
- Fix nullish coalescing to allow clearing config agent profile
- Expand StepPayload type with missing config-step fields
- Derive config agent Select value from store instead of local state
* fix: reuse persisted config chat session and reduce toolbar complexity
- Look up configChat.sessions for workspace before creating new session
- Extract MinimalToolbar and move defaults to module-level object to
reduce ChatInputToolbar cyclomatic complexity from 16 to 7
* fix: E2E executor test strict mode violation and invalid cleanup
- Use exact match for list_executors text to avoid matching 2 elements
- Remove cleanup that tried to delete pre-existing system executor
- Reuse persisted config chat session for workspace in global commands
- Reduce ChatInputToolbar cyclomatic complexity with module-level defaults
518 lines
17 KiB
Go
518 lines
17 KiB
Go
// Package sqlite provides SQLite-based repository implementations.
|
|
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// Repository provides SQLite-based task storage operations.
|
|
type Repository struct {
|
|
db *sqlx.DB // writer
|
|
ro *sqlx.DB // reader (read-only pool)
|
|
ownsDB bool
|
|
}
|
|
|
|
// NewWithDB creates a new SQLite repository with an existing database connection (shared ownership).
|
|
func NewWithDB(writer, reader *sqlx.DB) (*Repository, error) {
|
|
return newRepository(writer, reader, false)
|
|
}
|
|
|
|
func newRepository(writer, reader *sqlx.DB, ownsDB bool) (*Repository, error) {
|
|
repo := &Repository{db: writer, ro: reader, ownsDB: ownsDB}
|
|
if err := repo.initSchema(); err != nil {
|
|
if ownsDB {
|
|
if closeErr := writer.Close(); closeErr != nil {
|
|
return nil, fmt.Errorf("failed to close database after schema error: %w", closeErr)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (r *Repository) Close() error {
|
|
if !r.ownsDB {
|
|
return nil
|
|
}
|
|
return r.db.Close()
|
|
}
|
|
|
|
// DB returns the underlying sql.DB instance for shared access
|
|
func (r *Repository) DB() *sql.DB {
|
|
return r.db.DB
|
|
}
|
|
|
|
// ensureWorkspaceIndexes creates workspace-related indexes
|
|
func (r *Repository) ensureWorkspaceIndexes() error {
|
|
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id)`); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_workflows_workspace_id ON workflows(workspace_id)`); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workspace_archived ON tasks(workspace_id, archived_at)`); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ensureMessageMetadataIndexes creates indexes on JSON metadata fields for fast lookups.
|
|
func (r *Repository) ensureMessageMetadataIndexes() error {
|
|
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_messages_metadata_tool_call_id ON task_session_messages(task_session_id, json_extract(metadata, '$.tool_call_id'))`); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.db.Exec(`CREATE INDEX IF NOT EXISTS idx_messages_metadata_pending_id ON task_session_messages(task_session_id, json_extract(metadata, '$.pending_id'))`); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// initSchema creates the database tables if they don't exist
|
|
func (r *Repository) initSchema() error {
|
|
if err := r.initCoreSchema(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.initPlansSchema(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.initSessionSchema(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.initGitSchema(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.initReviewSchema(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.migrateExecutorProfiles(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.migrateTaskSessions(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.ensureDefaultWorkspace(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.ensureDefaultExecutorsAndEnvironments(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.runMigrations(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.ensureWorkspaceIndexes(); err != nil {
|
|
return err
|
|
}
|
|
return r.ensureMessageMetadataIndexes()
|
|
}
|
|
|
|
// migrateExecutorProfiles adds mcp_policy column and drops is_default from executor_profiles.
|
|
func (r *Repository) migrateExecutorProfiles() error {
|
|
// Add mcp_policy column if it doesn't exist
|
|
_, _ = r.db.Exec(`ALTER TABLE executor_profiles ADD COLUMN mcp_policy TEXT DEFAULT ''`)
|
|
// Drop is_default column - SQLite doesn't support DROP COLUMN before 3.35.0,
|
|
// so we just ignore the old column if present. New schema omits it.
|
|
return nil
|
|
}
|
|
|
|
// migrateTaskSessions adds new columns to task_sessions.
|
|
func (r *Repository) migrateTaskSessions() error {
|
|
_, _ = r.db.Exec(`ALTER TABLE task_sessions ADD COLUMN executor_profile_id TEXT DEFAULT ''`)
|
|
return nil
|
|
}
|
|
|
|
// runMigrations applies idempotent ALTER TABLE migrations for schema evolution.
|
|
func (r *Repository) runMigrations() error {
|
|
// Add last_message_uuid column to executors_running (ignore error if already exists)
|
|
_, _ = r.db.Exec(`ALTER TABLE executors_running ADD COLUMN last_message_uuid TEXT DEFAULT ''`)
|
|
// Add metadata column to executors_running (ignore error if already exists)
|
|
_, _ = r.db.Exec(`ALTER TABLE executors_running ADD COLUMN metadata TEXT DEFAULT '{}'`)
|
|
// Add is_ephemeral column to tasks for quick chat (ignore error if already exists)
|
|
_, _ = r.db.Exec(`ALTER TABLE tasks ADD COLUMN is_ephemeral INTEGER NOT NULL DEFAULT 0`)
|
|
// Add checkout_branch column to task_repositories (ignore error if already exists)
|
|
_, _ = r.db.Exec(`ALTER TABLE task_repositories ADD COLUMN checkout_branch TEXT DEFAULT ''`)
|
|
// Add base_commit_sha column to task_sessions for tracking session start commit (ignore error if already exists)
|
|
_, _ = r.db.Exec(`ALTER TABLE task_sessions ADD COLUMN base_commit_sha TEXT DEFAULT ''`)
|
|
// Add default_config_agent_profile_id column to workspaces (ignore error if already exists)
|
|
_, _ = r.db.Exec(`ALTER TABLE workspaces ADD COLUMN default_config_agent_profile_id TEXT DEFAULT ''`)
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) initCoreSchema() error {
|
|
if err := r.initInfraSchema(); err != nil {
|
|
return err
|
|
}
|
|
if err := r.initTaskSchema(); err != nil {
|
|
return err
|
|
}
|
|
return r.initCoreIndexes()
|
|
}
|
|
|
|
func (r *Repository) initInfraSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS workspaces (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
owner_id TEXT DEFAULT '',
|
|
default_executor_id TEXT DEFAULT '',
|
|
default_environment_id TEXT DEFAULT '',
|
|
default_agent_profile_id TEXT DEFAULT '',
|
|
default_config_agent_profile_id TEXT DEFAULT '',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS executors (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
resumable INTEGER NOT NULL DEFAULT 1,
|
|
config TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
deleted_at TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS executors_running (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL UNIQUE,
|
|
task_id TEXT NOT NULL,
|
|
executor_id TEXT NOT NULL,
|
|
runtime TEXT DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'starting',
|
|
resumable INTEGER NOT NULL DEFAULT 0,
|
|
resume_token TEXT DEFAULT '',
|
|
agent_execution_id TEXT DEFAULT '',
|
|
container_id TEXT DEFAULT '',
|
|
agentctl_url TEXT DEFAULT '',
|
|
agentctl_port INTEGER DEFAULT 0,
|
|
pid INTEGER DEFAULT 0,
|
|
worktree_id TEXT DEFAULT '',
|
|
worktree_path TEXT DEFAULT '',
|
|
worktree_branch TEXT DEFAULT '',
|
|
last_seen_at TIMESTAMP,
|
|
error_message TEXT DEFAULT '',
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS executor_profiles (
|
|
id TEXT PRIMARY KEY,
|
|
executor_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
mcp_policy TEXT DEFAULT '',
|
|
config TEXT DEFAULT '{}',
|
|
prepare_script TEXT DEFAULT '',
|
|
cleanup_script TEXT DEFAULT '',
|
|
env_vars TEXT DEFAULT '[]',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (executor_id) REFERENCES executors(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS environments (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
worktree_root TEXT DEFAULT '',
|
|
image_tag TEXT DEFAULT '',
|
|
dockerfile TEXT DEFAULT '',
|
|
build_config TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
deleted_at TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS workflows (
|
|
id TEXT PRIMARY KEY,
|
|
workspace_id TEXT NOT NULL DEFAULT '',
|
|
workflow_template_id TEXT DEFAULT '',
|
|
name TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initTaskSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id TEXT PRIMARY KEY,
|
|
workspace_id TEXT NOT NULL DEFAULT '',
|
|
workflow_id TEXT NOT NULL,
|
|
workflow_step_id TEXT NOT NULL DEFAULT '',
|
|
title TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
state TEXT DEFAULT 'TODO',
|
|
priority INTEGER DEFAULT 0,
|
|
position INTEGER DEFAULT 0,
|
|
metadata TEXT DEFAULT '{}',
|
|
archived_at TIMESTAMP,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS task_repositories (
|
|
id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL,
|
|
repository_id TEXT NOT NULL,
|
|
base_branch TEXT DEFAULT '',
|
|
position INTEGER DEFAULT 0,
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
|
|
UNIQUE(task_id, repository_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS repositories (
|
|
id TEXT PRIMARY KEY,
|
|
workspace_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
source_type TEXT NOT NULL DEFAULT 'local',
|
|
local_path TEXT DEFAULT '',
|
|
provider TEXT DEFAULT '',
|
|
provider_repo_id TEXT DEFAULT '',
|
|
provider_owner TEXT DEFAULT '',
|
|
provider_name TEXT DEFAULT '',
|
|
default_branch TEXT DEFAULT '',
|
|
worktree_branch_prefix TEXT DEFAULT 'feature/',
|
|
pull_before_worktree INTEGER NOT NULL DEFAULT 1,
|
|
setup_script TEXT DEFAULT '',
|
|
cleanup_script TEXT DEFAULT '',
|
|
dev_script TEXT DEFAULT '',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
deleted_at TIMESTAMP,
|
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS repository_scripts (
|
|
id TEXT PRIMARY KEY,
|
|
repository_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
command TEXT NOT NULL,
|
|
position INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
|
|
);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initCoreIndexes() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_tasks_workflow_id ON tasks(workflow_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tasks_workflow_step_id ON tasks(workflow_step_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tasks_archived_at ON tasks(archived_at);
|
|
CREATE INDEX IF NOT EXISTS idx_task_repositories_task_id ON task_repositories(task_id);
|
|
CREATE INDEX IF NOT EXISTS idx_task_repositories_repository_id ON task_repositories(repository_id);
|
|
CREATE INDEX IF NOT EXISTS idx_repositories_workspace_id ON repositories(workspace_id);
|
|
CREATE INDEX IF NOT EXISTS idx_repository_scripts_repo_id ON repository_scripts(repository_id);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initPlansSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS task_plans (
|
|
id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL DEFAULT 'Plan',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
created_by TEXT NOT NULL DEFAULT 'agent',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_task_plans_task_id ON task_plans(task_id);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initSessionSchema() error {
|
|
if err := r.initMessageTurnSchema(); err != nil {
|
|
return err
|
|
}
|
|
return r.initSessionWorktreeSchema()
|
|
}
|
|
|
|
func (r *Repository) initMessageTurnSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS task_session_messages (
|
|
id TEXT PRIMARY KEY,
|
|
task_session_id TEXT NOT NULL,
|
|
task_id TEXT DEFAULT '',
|
|
turn_id TEXT NOT NULL,
|
|
author_type TEXT NOT NULL DEFAULT 'user',
|
|
author_id TEXT DEFAULT '',
|
|
content TEXT NOT NULL,
|
|
requests_input INTEGER DEFAULT 0,
|
|
type TEXT NOT NULL DEFAULT 'message',
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (task_session_id) REFERENCES task_sessions(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (turn_id) REFERENCES task_session_turns(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON task_session_messages(task_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON task_session_messages(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_session_created ON task_session_messages(task_session_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_turn_id ON task_session_messages(turn_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS task_session_turns (
|
|
id TEXT PRIMARY KEY,
|
|
task_session_id TEXT NOT NULL,
|
|
task_id TEXT NOT NULL,
|
|
started_at TIMESTAMP NOT NULL,
|
|
completed_at TIMESTAMP,
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (task_session_id) REFERENCES task_sessions(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_turns_session_id ON task_session_turns(task_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_turns_session_started ON task_session_turns(task_session_id, started_at);
|
|
CREATE INDEX IF NOT EXISTS idx_turns_task_id ON task_session_turns(task_id);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initSessionWorktreeSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS task_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
task_id TEXT NOT NULL,
|
|
agent_execution_id TEXT NOT NULL DEFAULT '',
|
|
container_id TEXT NOT NULL DEFAULT '',
|
|
agent_profile_id TEXT NOT NULL,
|
|
executor_id TEXT DEFAULT '',
|
|
executor_profile_id TEXT DEFAULT '',
|
|
environment_id TEXT DEFAULT '',
|
|
repository_id TEXT DEFAULT '',
|
|
base_branch TEXT DEFAULT '',
|
|
agent_profile_snapshot TEXT DEFAULT '{}',
|
|
executor_snapshot TEXT DEFAULT '{}',
|
|
environment_snapshot TEXT DEFAULT '{}',
|
|
repository_snapshot TEXT DEFAULT '{}',
|
|
state TEXT NOT NULL DEFAULT 'CREATED',
|
|
error_message TEXT DEFAULT '',
|
|
metadata TEXT DEFAULT '{}',
|
|
started_at TIMESTAMP NOT NULL,
|
|
completed_at TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
is_primary INTEGER DEFAULT 0,
|
|
workflow_step_id TEXT DEFAULT '',
|
|
is_passthrough INTEGER DEFAULT 0,
|
|
review_status TEXT DEFAULT '',
|
|
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_task_sessions_task_id ON task_sessions(task_id);
|
|
CREATE INDEX IF NOT EXISTS idx_task_sessions_state ON task_sessions(state);
|
|
CREATE INDEX IF NOT EXISTS idx_task_sessions_task_state ON task_sessions(task_id, state);
|
|
|
|
CREATE TABLE IF NOT EXISTS task_session_worktrees (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL,
|
|
worktree_id TEXT NOT NULL,
|
|
repository_id TEXT NOT NULL,
|
|
position INTEGER DEFAULT 0,
|
|
worktree_path TEXT DEFAULT '',
|
|
worktree_branch TEXT DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
merged_at TIMESTAMP,
|
|
deleted_at TIMESTAMP,
|
|
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE,
|
|
UNIQUE(session_id, worktree_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_session_id ON task_session_worktrees(session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_worktree_id ON task_session_worktrees(worktree_id);
|
|
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_repository_id ON task_session_worktrees(repository_id);
|
|
CREATE INDEX IF NOT EXISTS idx_task_session_worktrees_status ON task_session_worktrees(status);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initGitSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS task_session_git_snapshots (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL,
|
|
snapshot_type TEXT NOT NULL,
|
|
branch TEXT NOT NULL,
|
|
remote_branch TEXT DEFAULT '',
|
|
head_commit TEXT DEFAULT '',
|
|
base_commit TEXT DEFAULT '',
|
|
ahead INTEGER DEFAULT 0,
|
|
behind INTEGER DEFAULT 0,
|
|
files TEXT DEFAULT '{}',
|
|
triggered_by TEXT DEFAULT '',
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_git_snapshots_session ON task_session_git_snapshots(session_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_git_snapshots_type ON task_session_git_snapshots(session_id, snapshot_type);
|
|
|
|
CREATE TABLE IF NOT EXISTS task_session_commits (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL,
|
|
commit_sha TEXT NOT NULL,
|
|
parent_sha TEXT DEFAULT '',
|
|
author_name TEXT DEFAULT '',
|
|
author_email TEXT DEFAULT '',
|
|
commit_message TEXT DEFAULT '',
|
|
committed_at TIMESTAMP NOT NULL,
|
|
pre_commit_snapshot_id TEXT DEFAULT '',
|
|
post_commit_snapshot_id TEXT DEFAULT '',
|
|
files_changed INTEGER DEFAULT 0,
|
|
insertions INTEGER DEFAULT 0,
|
|
deletions INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_session_commits_session ON task_session_commits(session_id, committed_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_session_commits_sha ON task_session_commits(commit_sha);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) initReviewSchema() error {
|
|
_, err := r.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS session_file_reviews (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL,
|
|
file_path TEXT NOT NULL,
|
|
reviewed INTEGER NOT NULL DEFAULT 0,
|
|
diff_hash TEXT NOT NULL DEFAULT '',
|
|
reviewed_at TIMESTAMP,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (session_id) REFERENCES task_sessions(id) ON DELETE CASCADE,
|
|
UNIQUE(session_id, file_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_session_file_reviews_session ON session_file_reviews(session_id);
|
|
`)
|
|
return err
|
|
}
|