1b58a64fd8
* feat: add automation system with triggers, backend-driven metadata, and PR association Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(automations): execution_mode + flat /settings/automations Layered on top of #406's automation subsystem: - Add execution_mode to Automation (task | run, default task). Run-mode firings create an ephemeral kanban-hidden task tagged with origin=automation_run. The session pipeline still runs against it, but users surface them through the AutomationRun history instead of the kanban. Pragmatic shim while AutomationRun-as-true-session-owner is deferred to a follow-up. - Thread IsEphemeral + Origin through ReviewTaskRequest + reviewTaskCreatorAdapter so the orchestrator can create the hidden task in one call. - Run-mode always auto-starts (the user never sees the task to drag). - Flat /settings/automations route replaces the per-workspace sidebar entry. Picks the only workspace, shows a picker for many, or shows a CTA for zero workspaces. - Editor exposes execution mode + table shows it as a badge column. Spec at docs/specs/office/automations-settings.md documents the design, deferred items, and scenarios. * feat(automations): per-automation repository picker Adds a repository_id field on Automation so users can pin scheduled and webhook automations to a specific workspace repository instead of always falling through to "first repo". github_pr triggers ignore the field and keep using the PR's own repo — the editor disables the picker with a hint when a PR trigger is selected. Backend: - repository_id column folded into the canonical CREATE TABLE (no migration needed — PR #406 introduces the table). - RepositoryID threaded through model, DTOs, store INSERT/UPDATE, and service.CreateAutomation. - resolveAutomationRepository prefers the explicit ID, falls back to workspace-first on empty. Frontend: - repository_id on the Automation TS type + Create/Update payloads. - Repository SelectField in ConfigSection backed by useRepositories; "Auto" sentinel represents the empty/fallback choice. - Disabled with help-text for github_pr triggers. * feat(automations): repo picker shows discovered repos too Matches the task-creation dialog's UX: the editor's repository picker now lists registered workspace repos AND filesystem-discovered repos under the workspace's roots. Picking a discovered repo registers it with the workspace at save time (via createRepositoryAction), captures the new id, and writes it onto the automation. After the first save the picker selection is promoted from "discovered" to "registered" so re-saving doesn't re-register. Form state holds a RepositorySelection union (auto | registered | discovered) rather than a plain id string so the picker can carry the metadata needed to register. github_pr triggers still disable the picker — PR events always override any configured repo with the PR's own. * fix(automations): add idempotent ALTERs for in-branch columns The canonical CREATE TABLE covers fresh installs, but `CREATE TABLE IF NOT EXISTS` is a no-op for DBs already initialised from an earlier commit on this branch (PR #406's original schema). Without these ALTERs, dev DBs created before execution_mode + repository_id existed on the row hit "no such column" errors. Three idempotent ALTERs: task_title_template, execution_mode, repository_id. SQLite returns a duplicate-column error when the column already exists; we swallow it. * fix(automations): use generateUUID in non-secure contexts crypto.randomUUID is only defined in secure contexts (HTTPS or localhost). PR #406's editor used it directly, which throws "crypto. randomUUID is not a function" when the app is reached via a plain hostname like nova:58204. Switch to lib/utils.generateUUID, which falls back to a Math.random-based implementation when crypto isn't available. * fix(automations): polish run-mode lifecycle, validation, and editor UX Run-mode automations were partly broken end-to-end: - workflow_id was sent for ephemeral tasks (rejected by task service) and was required by the editor + automation service, so "Create Automation" was always disabled and silently failed - a permission_request hanging the agent left the run pinned at task_created forever; nothing reaped the ephemeral worktree - trigger config inputs (cron, branches, authors) did not re-sync with the loaded automation on page reload - runs row never linked back to the underlying task for task-mode automations, and the table header highlighted on hover Backend - skip workflow fields in CreateReviewTask / StartTask for run-mode - relax automation.Service.CreateAutomation: workflow only required for task-mode - auto-reject permission prompts on ephemeral automation tasks and mark the AutomationRun failed with the requested action - on agent completion / failure for ephemeral automation tasks, flip the run to succeeded/failed and remove the worktree + branch immediately (new RunStatusSucceeded + MarkRunSucceededByTaskID) - wire worktree.Manager into the orchestrator service Frontend - show Execution Mode first; hide Workflow / Workflow Step in run-mode - canSave no longer requires workflow in run-mode - surface save errors via toast instead of silent failure - sync trigger-config local input state with loaded prop value - runs table: clickable row → /tasks/<id> for task-mode runs, non-clickable in run-mode; suppress header hover * fix(automations): address PR review feedback Backend - automation/scheduler.go: parseCronFields returns an error for unsupported expressions instead of silently defaulting to hourly - automation/webhook.go: constant-time secret compare, header-only (no more ?secret= query param), reject with 409 when no enabled webhook trigger exists - automation/service.go: enforce max_concurrent_runs in FireTrigger via maybeSkipForConcurrencyCap (records a skipped run when capped) - cmd/kandev/e2e_reset.go: surface automation delete errors as 500 instead of swallowing them - orchestrator/event_handlers_git.go: auto-reject permission with cancelled=true so the message status is "rejected" and the session isn't flipped back to running - orchestrator/event_handlers_agent.go: finalizeAutomationRunIfEphemeral now runs at the top of handleAgentFailed, before the resume/recoverable early returns, so session-backed run-mode failures get cleaned up Frontend - state/slices/automations/automations-slice.ts: declare _get/_api so the call site's arity matches and CodeQL stops flagging ignored trailing arguments - automations/schedule-selector.tsx: clearing the input now propagates ""; cron is trimmed before validation+save - automations/triggers-section.tsx: switching to webhook mode deletes the existing scheduled/condition triggers so they don't keep firing hidden in the background - automations/trigger-configs/github-ci-config.tsx: useEffect re-sync for checkNames, matching the other trigger configs - hooks/domains/settings/use-automations.ts: per-workspace fetch via loadedWorkspaceRef + render-safe loadedWorkspaceId state; inFlightWorkspaceRef discards stale responses after workspace switch --------- Co-authored-by: Carlos Florêncio <carlosmflorencio@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kandev Agent <agent@kandev.dev>
927 lines
39 KiB
Go
927 lines
39 KiB
Go
package models
|
|
|
|
import (
|
|
"errors"
|
|
"maps"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/agentruntime"
|
|
"github.com/kandev/kandev/internal/sysprompt"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
)
|
|
|
|
// ErrExecutorRunningNotFound is returned when no executor running record exists for a session.
|
|
var ErrExecutorRunningNotFound = errors.New("executor running not found")
|
|
|
|
// ErrExecutionRotated is returned by CAS-style updates on executors_running when the
|
|
// row's agent_execution_id has rotated since the caller observed it. Indicates the
|
|
// caller's write target a now-defunct execution and should be discarded — typically
|
|
// a stale resume-token event from an execution that was replaced (model switch,
|
|
// context reset, fresh re-launch). Callers should not retry; the new execution
|
|
// will produce its own events.
|
|
var ErrExecutionRotated = errors.New("execution rotated; CAS write rejected")
|
|
|
|
// ListMessagesOptions defines pagination options for listing messages
|
|
type ListMessagesOptions struct {
|
|
Limit int
|
|
Before string
|
|
After string
|
|
Sort string
|
|
}
|
|
|
|
// SearchMessagesOptions defines options for searching a session's messages.
|
|
type SearchMessagesOptions struct {
|
|
Query string
|
|
Limit int
|
|
}
|
|
|
|
// Task metadata keys used for deferred agent start (e.g., task.moved → handleTaskMovedNoSession).
|
|
const (
|
|
MetaKeyAgentProfileID = "agent_profile_id"
|
|
MetaKeyExecutorProfileID = "executor_profile_id"
|
|
// MetaKeyWorkspacePath is the optional host folder for repo-less tasks
|
|
// (set by CreateTask, read by the orchestrator when building a session).
|
|
// Centralised here so the set/read sites can't drift apart.
|
|
MetaKeyWorkspacePath = "workspace_path"
|
|
)
|
|
|
|
// TaskSession.Metadata key that records how the session came into existence.
|
|
// Read by maybySwitchSessionForProfile to decide whether transitioning to a
|
|
// step with no agent_profile override should revert to the task default:
|
|
// workflow-spawned sessions (created_by=workflow_switch) -> yes, revert.
|
|
// user-created sessions (no created_by tag) -> no, preserve.
|
|
const (
|
|
SessionMetaKeyCreatedBy = "created_by"
|
|
SessionCreatedByWorkflowSwitch = "workflow_switch"
|
|
)
|
|
|
|
// Task origin values for the Origin field.
|
|
const (
|
|
TaskOriginManual = "manual"
|
|
TaskOriginAgentCreated = "agent_created"
|
|
TaskOriginRoutine = "routine"
|
|
TaskOriginOnboarding = "onboarding"
|
|
TaskOriginAutomationRun = "automation_run"
|
|
)
|
|
|
|
// Task represents a task in the database
|
|
type Task struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
State v1.TaskState `json:"state"`
|
|
Priority string `json:"priority"`
|
|
Position int `json:"position"` // Order within workflow step
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
Repositories []*TaskRepository `json:"repositories,omitempty"`
|
|
IsEphemeral bool `json:"is_ephemeral"` // Ephemeral tasks are not shown in kanban, used for quick chat
|
|
ParentID string `json:"parent_id,omitempty"` // FK to parent task for subtasks
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
// ArchivedByCascadeID is set when the task was archived as part of an
|
|
// office task-handoffs cascade (phase 6). UnarchiveTaskByCascade uses
|
|
// it to scope restoration to exactly the descendants this cascade
|
|
// owned, leaving manually-archived tasks alone. Empty for
|
|
// manually-archived tasks and any task archived before phase 6.
|
|
ArchivedByCascadeID string `json:"archived_by_cascade_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Office extensions.
|
|
//
|
|
// ADR 0005 Wave F: the task's assignee is no longer a column on the
|
|
// tasks table. The canonical store is a `runner` row in
|
|
// workflow_step_participants (resolve via
|
|
// workflow.Repository.ResolveCurrentRunner). The struct field below
|
|
// is a read-time projection populated by repo SELECT queries through
|
|
// a correlated subquery and consumed unchanged by callers that
|
|
// previously read tasks.assignee_agent_profile_id. Repo writes to
|
|
// this field are routed to SetTaskRunner / ClearTaskRunner inside
|
|
// the task repository.
|
|
AssigneeAgentProfileID string `json:"assignee_agent_profile_id,omitempty"`
|
|
Origin string `json:"origin,omitempty"` // manual, agent_created, routine
|
|
ProjectID string `json:"project_id,omitempty"` // FK to office project
|
|
Labels string `json:"labels,omitempty"` // JSON array string, default "[]"
|
|
Identifier string `json:"identifier,omitempty"` // e.g. "KAN-42"
|
|
|
|
// IsFromOffice is a read-time projection set by the task repo's SELECT
|
|
// (see isFromOfficeProjection in repository/sqlite/task.go). True when
|
|
// the task is owned by office: either it has a non-empty ProjectID, or
|
|
// its workflow matches the workspace's office_workflow_id. Kanban-board
|
|
// tasks always come back false. UI callers gate office-only surfaces on
|
|
// this (e.g. the "Open in office view" topbar link).
|
|
IsFromOffice bool `json:"is_from_office,omitempty"`
|
|
}
|
|
|
|
// TaskTreeFilters provides filter options for the task tree query.
|
|
type TaskTreeFilters struct {
|
|
ProjectID string
|
|
AssigneeID string
|
|
WorkflowID string
|
|
Origin string
|
|
}
|
|
|
|
// WorkflowStyle values are persisted in workflows.style. See ADR-0004.
|
|
// They're a UX hint for the frontend; backend code MUST NOT branch on them.
|
|
const (
|
|
WorkflowStyleKanban = "kanban"
|
|
WorkflowStyleOffice = "office"
|
|
WorkflowStyleCustom = "custom"
|
|
)
|
|
|
|
// Workflow represents a task workflow
|
|
type Workflow struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
WorkflowTemplateID *string `json:"workflow_template_id,omitempty"`
|
|
SortOrder int `json:"sort_order"`
|
|
// Hidden workflows are excluded from management and picker UIs by default.
|
|
// Used by system-only flows like Improve Kandev.
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
// Style is a Phase 2 (ADR-0004) UX hint read by the frontend ONLY.
|
|
// Allowed values: "kanban" | "office" | "custom". Empty / unknown
|
|
// values fall back to "kanban" via the schema default. Backend code
|
|
// MUST NOT branch on this field — it is a presentation hint, not a
|
|
// behavioural invariant.
|
|
Style string `json:"style,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Workspace represents a workspace
|
|
type Workspace struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
OwnerID string `json:"owner_id"`
|
|
DefaultExecutorID *string `json:"default_executor_id,omitempty"`
|
|
DefaultEnvironmentID *string `json:"default_environment_id,omitempty"`
|
|
DefaultAgentProfileID *string `json:"default_agent_profile_id,omitempty"`
|
|
DefaultConfigAgentProfileID *string `json:"default_config_agent_profile_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Office extensions
|
|
TaskPrefix string `json:"task_prefix,omitempty"` // e.g. "KAN"
|
|
TaskSequence int `json:"task_sequence,omitempty"` // auto-incrementing counter
|
|
OfficeWorkflowID string `json:"office_workflow_id,omitempty"` // FK to system office workflow
|
|
}
|
|
|
|
// TaskRepository represents a repository associated with a task
|
|
type TaskRepository struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
BaseBranch string `json:"base_branch"`
|
|
CheckoutBranch string `json:"checkout_branch,omitempty"`
|
|
Position int `json:"position"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// MessageAuthorType represents who authored a message
|
|
type MessageAuthorType string
|
|
|
|
const (
|
|
// MessageAuthorUser indicates a message from a human user
|
|
MessageAuthorUser MessageAuthorType = "user"
|
|
// MessageAuthorAgent indicates a message from an AI agent
|
|
MessageAuthorAgent MessageAuthorType = "agent"
|
|
)
|
|
|
|
// MessageType represents the type of message content
|
|
type MessageType string
|
|
|
|
const (
|
|
// MessageTypeMessage is the default type for user/agent regular messages
|
|
MessageTypeMessage MessageType = "message"
|
|
// MessageTypeContent is for agent response content
|
|
MessageTypeContent MessageType = "content"
|
|
// MessageTypeToolCall is when agent uses a tool
|
|
MessageTypeToolCall MessageType = "tool_call"
|
|
// MessageTypeToolEdit is for file edit operations with diff visualization
|
|
MessageTypeToolEdit MessageType = "tool_edit"
|
|
// MessageTypeToolRead is for file read operations
|
|
MessageTypeToolRead MessageType = "tool_read"
|
|
// MessageTypeToolExecute is for command execution operations
|
|
MessageTypeToolExecute MessageType = "tool_execute"
|
|
// MessageTypeProgress is for progress updates
|
|
MessageTypeProgress MessageType = "progress"
|
|
// MessageTypeLog is for agent log messages (info, debug, warning, etc.)
|
|
MessageTypeLog MessageType = "log"
|
|
// MessageTypeError is for error messages
|
|
MessageTypeError MessageType = "error"
|
|
// MessageTypeStatus is for status changes: started, completed, failed
|
|
MessageTypeStatus MessageType = "status"
|
|
// MessageTypePermissionRequest is for agent permission requests
|
|
MessageTypePermissionRequest MessageType = "permission_request"
|
|
// MessageTypeClarificationRequest is for agent clarification questions
|
|
MessageTypeClarificationRequest MessageType = "clarification_request"
|
|
// MessageTypeScriptExecution is for setup/cleanup script execution messages
|
|
MessageTypeScriptExecution MessageType = "script_execution"
|
|
// MessageTypeThinking is for agent thinking/reasoning content
|
|
MessageTypeThinking MessageType = "thinking"
|
|
// MessageTypeAgentPlan is for agent native plan content (e.g. ExitPlanMode)
|
|
MessageTypeAgentPlan MessageType = "agent_plan"
|
|
// MessageTypeTodo is for agent todo/task list updates
|
|
MessageTypeTodo MessageType = "todo"
|
|
)
|
|
|
|
// Message represents a message in a task session
|
|
type Message struct {
|
|
ID string `json:"id"`
|
|
TaskSessionID string `json:"session_id"`
|
|
TaskID string `json:"task_id,omitempty"`
|
|
TurnID string `json:"turn_id"` // FK to task_session_turns
|
|
AuthorType MessageAuthorType `json:"author_type"`
|
|
AuthorID string `json:"author_id,omitempty"` // User ID or Agent Execution ID
|
|
Content string `json:"content"`
|
|
Type MessageType `json:"type,omitempty"` // Defaults to "message"
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
RequestsInput bool `json:"requests_input"` // True if agent is requesting user input
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// ToAPI converts internal Message to API type.
|
|
// Only true system-injected content (wrapped in <kandev-system> tags) is stripped
|
|
// from the visible content sent to the UI.
|
|
func (m *Message) ToAPI() *v1.Message {
|
|
messageType := string(m.Type)
|
|
if messageType == "" {
|
|
messageType = string(MessageTypeMessage)
|
|
}
|
|
hasHidden := sysprompt.HasSystemContent(m.Content)
|
|
meta := m.Metadata
|
|
if hasHidden {
|
|
if meta == nil {
|
|
meta = make(map[string]interface{})
|
|
} else {
|
|
// Copy to avoid mutating original
|
|
meta = copyMetadata(meta)
|
|
}
|
|
meta["has_hidden_prompts"] = true
|
|
}
|
|
result := &v1.Message{
|
|
ID: m.ID,
|
|
TaskSessionID: m.TaskSessionID,
|
|
TaskID: m.TaskID,
|
|
TurnID: m.TurnID,
|
|
AuthorType: string(m.AuthorType),
|
|
AuthorID: m.AuthorID,
|
|
Content: sysprompt.StripSystemContent(m.Content),
|
|
Type: messageType,
|
|
Metadata: meta,
|
|
RequestsInput: m.RequestsInput,
|
|
CreatedAt: m.CreatedAt,
|
|
}
|
|
if hasHidden {
|
|
result.RawContent = m.Content
|
|
}
|
|
return result
|
|
}
|
|
|
|
// copyMetadata returns a shallow copy of a metadata map.
|
|
func copyMetadata(m map[string]any) map[string]any {
|
|
cp := make(map[string]any, len(m))
|
|
maps.Copy(cp, m)
|
|
return cp
|
|
}
|
|
|
|
// Turn represents a single prompt/response cycle within a task session.
|
|
// A turn starts when a user sends a prompt and ends when the agent completes,
|
|
// cancels, or errors.
|
|
type Turn struct {
|
|
ID string `json:"id"`
|
|
TaskSessionID string `json:"session_id"`
|
|
TaskID string `json:"task_id"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ReviewStatus represents the review state of a TaskSession. The zero value
|
|
// (ReviewStatusNone, the empty string) means "no review needed" and serializes
|
|
// out via omitempty, preserving the JSON shape of the legacy *string field.
|
|
// The persisted column (task_sessions.review_status TEXT DEFAULT ”) is
|
|
// compatible with these values without any migration.
|
|
type ReviewStatus string
|
|
|
|
const (
|
|
// ReviewStatusNone is the zero value meaning no review is needed.
|
|
ReviewStatusNone ReviewStatus = ""
|
|
// ReviewStatusPending indicates the session is waiting on a human review.
|
|
ReviewStatusPending ReviewStatus = "pending"
|
|
// ReviewStatusApproved indicates the reviewer accepted the session output.
|
|
ReviewStatusApproved ReviewStatus = "approved"
|
|
)
|
|
|
|
// String returns the underlying string value for the review status.
|
|
func (r ReviewStatus) String() string { return string(r) }
|
|
|
|
// TaskSessionState represents the state of an agent session
|
|
type TaskSessionState string
|
|
|
|
const (
|
|
// TaskSessionStateCreated - session created but agent not started
|
|
TaskSessionStateCreated TaskSessionState = "CREATED"
|
|
// TaskSessionStateStarting - agent is starting up
|
|
TaskSessionStateStarting TaskSessionState = "STARTING"
|
|
// TaskSessionStateRunning - agent is actively running
|
|
TaskSessionStateRunning TaskSessionState = "RUNNING"
|
|
// TaskSessionStateIdle - office sessions only: agent process and executor torn down
|
|
// between turns; conversation (acp_session_id) preserved for the next run.
|
|
TaskSessionStateIdle TaskSessionState = "IDLE"
|
|
// TaskSessionStateWaitingForInput - agent waiting for user input
|
|
TaskSessionStateWaitingForInput TaskSessionState = "WAITING_FOR_INPUT"
|
|
// TaskSessionStateCompleted - agent finished successfully
|
|
TaskSessionStateCompleted TaskSessionState = "COMPLETED"
|
|
// TaskSessionStateFailed - agent failed with error
|
|
TaskSessionStateFailed TaskSessionState = "FAILED"
|
|
// TaskSessionStateCancelled - agent was manually stopped
|
|
TaskSessionStateCancelled TaskSessionState = "CANCELLED"
|
|
)
|
|
|
|
// TaskSessionWorktree represents the association between a task session and a worktree
|
|
type TaskSessionWorktree struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
WorktreeID string `json:"worktree_id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
Position int `json:"position"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// Worktree details stored on this association
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
}
|
|
|
|
// SessionBranchInfo is a lightweight projection of a session with its worktree branch.
|
|
// Used by the PR watch reconciler to find sessions that may need PR watches.
|
|
type SessionBranchInfo struct {
|
|
SessionID string
|
|
TaskID string
|
|
Branch string
|
|
}
|
|
|
|
// TaskSession represents a persistent agent execution session for a task.
|
|
// This replaces the in-memory TaskExecution tracking and survives backend restarts.
|
|
type TaskSession struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
AgentExecutionID string `json:"agent_execution_id"` // Docker container/agent execution
|
|
ContainerID string `json:"container_id"` // Docker container ID for cleanup
|
|
AgentProfileID string `json:"agent_profile_id"` // ID of the agent profile used
|
|
ExecutorID string `json:"executor_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id"`
|
|
EnvironmentID string `json:"environment_id"`
|
|
RepositoryID string `json:"repository_id"` // Primary repository (for backward compatibility)
|
|
BaseBranch string `json:"base_branch"` // Primary base branch (for backward compatibility)
|
|
BaseCommitSHA string `json:"base_commit_sha"` // Git commit SHA at session start (for cumulative diff)
|
|
WorkspacePath string `json:"workspace_path"` // Optional host folder for repo-less tasks (when user picked a starting folder)
|
|
Worktrees []*TaskSessionWorktree `json:"worktrees,omitempty"` // Associated worktrees
|
|
AgentProfileSnapshot map[string]interface{} `json:"agent_profile_snapshot,omitempty"`
|
|
ExecutorSnapshot map[string]interface{} `json:"executor_snapshot,omitempty"`
|
|
EnvironmentSnapshot map[string]interface{} `json:"environment_snapshot,omitempty"`
|
|
RepositorySnapshot map[string]interface{} `json:"repository_snapshot,omitempty"`
|
|
State TaskSessionState `json:"state"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Environment reference
|
|
TaskEnvironmentID string `json:"task_environment_id,omitempty"` // FK to task_environments for shared env
|
|
|
|
// Workflow-related fields
|
|
IsPrimary bool `json:"is_primary"` // Whether this is the primary session for the task
|
|
IsPassthrough bool `json:"is_passthrough"` // Whether this session uses passthrough (PTY) mode
|
|
ReviewStatus ReviewStatus `json:"review_status,omitempty"` // zero value = no review needed
|
|
}
|
|
|
|
// ToAPI converts internal TaskSession to API type
|
|
// TODO: Add v1.TaskSession type to pkg/api/v1/
|
|
func (s *TaskSession) ToAPI() map[string]interface{} {
|
|
result := map[string]interface{}{
|
|
"id": s.ID,
|
|
"task_id": s.TaskID,
|
|
"agent_execution_id": s.AgentExecutionID,
|
|
"container_id": s.ContainerID,
|
|
"agent_profile_id": s.AgentProfileID,
|
|
"executor_id": s.ExecutorID,
|
|
"executor_profile_id": s.ExecutorProfileID,
|
|
"environment_id": s.EnvironmentID,
|
|
"repository_id": s.RepositoryID,
|
|
"base_branch": s.BaseBranch,
|
|
"base_commit_sha": s.BaseCommitSHA,
|
|
"worktrees": s.Worktrees,
|
|
"state": string(s.State),
|
|
"started_at": s.StartedAt,
|
|
"updated_at": s.UpdatedAt,
|
|
}
|
|
// For backward compatibility, populate worktree_path and worktree_branch from first worktree
|
|
if len(s.Worktrees) > 0 {
|
|
result["worktree_path"] = s.Worktrees[0].WorktreePath
|
|
result["worktree_branch"] = s.Worktrees[0].WorktreeBranch
|
|
}
|
|
if s.ErrorMessage != "" {
|
|
result["error_message"] = s.ErrorMessage
|
|
}
|
|
if s.CompletedAt != nil {
|
|
result["completed_at"] = s.CompletedAt
|
|
}
|
|
if s.Metadata != nil {
|
|
result["metadata"] = s.Metadata
|
|
}
|
|
if s.AgentProfileSnapshot != nil {
|
|
result["agent_profile_snapshot"] = s.AgentProfileSnapshot
|
|
}
|
|
if s.ExecutorSnapshot != nil {
|
|
result["executor_snapshot"] = s.ExecutorSnapshot
|
|
}
|
|
if s.EnvironmentSnapshot != nil {
|
|
result["environment_snapshot"] = s.EnvironmentSnapshot
|
|
}
|
|
if s.RepositorySnapshot != nil {
|
|
result["repository_snapshot"] = s.RepositorySnapshot
|
|
}
|
|
result["is_passthrough"] = s.IsPassthrough
|
|
if s.TaskEnvironmentID != "" {
|
|
result["task_environment_id"] = s.TaskEnvironmentID
|
|
}
|
|
// Office sessions carry an agent_profile_id; the frontend uses it
|
|
// to distinguish per-(task, agent) office sessions from per-launch
|
|
// kanban/quick-chat sessions, which drives "live" detection in the
|
|
// chat. Omit when blank so kanban responses look unchanged.
|
|
if s.AgentProfileID != "" {
|
|
result["agent_profile_id"] = s.AgentProfileID
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Repository represents a workspace repository
|
|
type Repository struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
SourceType string `json:"source_type"`
|
|
// LocalPath is the path to a local checkout; for provider-backed repos, this is
|
|
// populated after the repo is cloned/synced on the agent host.
|
|
LocalPath string `json:"local_path"`
|
|
// Provider fields describe the upstream source (e.g. github/gitlab) for future syncing.
|
|
Provider string `json:"provider"`
|
|
ProviderRepoID string `json:"provider_repo_id"`
|
|
ProviderOwner string `json:"provider_owner"`
|
|
ProviderName string `json:"provider_name"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
WorktreeBranchPrefix string `json:"worktree_branch_prefix"`
|
|
PullBeforeWorktree bool `json:"pull_before_worktree"`
|
|
SetupScript string `json:"setup_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
DevScript string `json:"dev_script"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
|
}
|
|
|
|
// RepositoryScript represents a custom script for a repository
|
|
type RepositoryScript struct {
|
|
ID string `json:"id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
Name string `json:"name"`
|
|
Command string `json:"command"`
|
|
Position int `json:"position"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ExecutorType represents the executor runtime type.
|
|
type ExecutorType string
|
|
|
|
const (
|
|
ExecutorTypeLocal ExecutorType = "local"
|
|
ExecutorTypeWorktree ExecutorType = "worktree"
|
|
ExecutorTypeLocalDocker ExecutorType = "local_docker"
|
|
ExecutorTypeRemoteDocker ExecutorType = "remote_docker"
|
|
ExecutorTypeSprites ExecutorType = "sprites"
|
|
ExecutorTypeMockRemote ExecutorType = "mock_remote"
|
|
)
|
|
|
|
// IsRemoteExecutorType reports whether the given executor type represents
|
|
// a remote execution environment (including containerized environments like Docker).
|
|
// These environments run shells inside the container/VM, not on the host.
|
|
func IsRemoteExecutorType(t ExecutorType) bool {
|
|
switch t {
|
|
case ExecutorTypeSprites, ExecutorTypeRemoteDocker, ExecutorTypeLocalDocker, ExecutorTypeMockRemote:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Runtime maps an ExecutorType to the runtime backend that hosts the
|
|
// agent subprocess. Mirrors executor.ExecutorTypeToBackend so callers
|
|
// outside the executor package can ask the property without importing
|
|
// it. The two are kept in lock-step intentionally — keep them in sync
|
|
// when adding a new ExecutorType.
|
|
func (t ExecutorType) Runtime() agentruntime.Runtime {
|
|
switch t {
|
|
case ExecutorTypeLocal, ExecutorTypeWorktree, ExecutorTypeMockRemote:
|
|
return agentruntime.RuntimeStandalone
|
|
case ExecutorTypeLocalDocker:
|
|
return agentruntime.RuntimeDocker
|
|
case ExecutorTypeRemoteDocker:
|
|
return agentruntime.RuntimeRemoteDocker
|
|
case ExecutorTypeSprites:
|
|
return agentruntime.RuntimeSprites
|
|
default:
|
|
return agentruntime.RuntimeStandalone
|
|
}
|
|
}
|
|
|
|
// IsContainerizedExecutorType reports whether the given executor type runs
|
|
// in a container/sandbox where shells must be executed inside the container
|
|
// via agentctl, not on the host. Single source of truth is
|
|
// Runtime().IsContainerized() so a new ExecutorType only has to declare its
|
|
// runtime once.
|
|
func IsContainerizedExecutorType(t ExecutorType) bool {
|
|
return t.Runtime().IsContainerized()
|
|
}
|
|
|
|
// IsAlwaysResumableRuntime reports whether the given runtime represents
|
|
// an executor that can always be resumed even without an explicit resume token.
|
|
func IsAlwaysResumableRuntime(runtime agentruntime.Runtime) bool {
|
|
return runtime == agentruntime.RuntimeSprites
|
|
}
|
|
|
|
const (
|
|
ExecutorIDLocal = "exec-local"
|
|
ExecutorIDWorktree = "exec-worktree"
|
|
ExecutorIDLocalDocker = "exec-local-docker"
|
|
ExecutorIDSprites = "exec-sprites"
|
|
)
|
|
|
|
// ExecutorStatus represents executor availability.
|
|
type ExecutorStatus string
|
|
|
|
const (
|
|
ExecutorStatusActive ExecutorStatus = "active"
|
|
ExecutorStatusDisabled ExecutorStatus = "disabled"
|
|
)
|
|
|
|
// Executor represents an execution target.
|
|
type Executor struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type ExecutorType `json:"type"`
|
|
Status ExecutorStatus `json:"status"`
|
|
IsSystem bool `json:"is_system"`
|
|
Resumable bool `json:"resumable"`
|
|
Config map[string]string `json:"config,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
|
}
|
|
|
|
// ExecutorRunning tracks an active executor instance for a session.
|
|
type ExecutorRunning struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
TaskID string `json:"task_id"`
|
|
ExecutorID string `json:"executor_id"`
|
|
Runtime agentruntime.Runtime `json:"runtime,omitempty"`
|
|
Status string `json:"status"`
|
|
Resumable bool `json:"resumable"`
|
|
ResumeToken string `json:"resume_token,omitempty"`
|
|
LastMessageUUID string `json:"last_message_uuid,omitempty"`
|
|
AgentExecutionID string `json:"agent_execution_id,omitempty"`
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
AgentctlURL string `json:"agentctl_url,omitempty"`
|
|
AgentctlPort int `json:"agentctl_port,omitempty"`
|
|
PID int `json:"pid,omitempty"`
|
|
WorktreeID string `json:"worktree_id,omitempty"`
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ProfileEnvVar represents an environment variable for an executor profile.
|
|
// Either Value (plain text) or SecretID (reference to a secret) should be set, not both.
|
|
type ProfileEnvVar struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value,omitempty"`
|
|
SecretID string `json:"secret_id,omitempty"`
|
|
}
|
|
|
|
// ExecutorProfile represents a named configuration preset for an executor.
|
|
type ExecutorProfile struct {
|
|
ID string `json:"id"`
|
|
ExecutorID string `json:"executor_id"`
|
|
Name string `json:"name"`
|
|
McpPolicy string `json:"mcp_policy,omitempty"`
|
|
Config map[string]string `json:"config,omitempty"`
|
|
PrepareScript string `json:"prepare_script,omitempty"`
|
|
CleanupScript string `json:"cleanup_script,omitempty"`
|
|
EnvVars []ProfileEnvVar `json:"env_vars,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// EnvironmentKind represents the runtime type for environments.
|
|
type EnvironmentKind string
|
|
|
|
const (
|
|
EnvironmentKindLocalPC EnvironmentKind = "local_pc"
|
|
EnvironmentKindDockerImage EnvironmentKind = "docker_image"
|
|
)
|
|
|
|
const (
|
|
EnvironmentIDLocal = "env-local"
|
|
)
|
|
|
|
// Environment represents a runtime environment configuration.
|
|
type Environment struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Kind EnvironmentKind `json:"kind"`
|
|
IsSystem bool `json:"is_system"`
|
|
WorktreeRoot string `json:"worktree_root,omitempty"`
|
|
ImageTag string `json:"image_tag,omitempty"`
|
|
Dockerfile string `json:"dockerfile,omitempty"`
|
|
BuildConfig map[string]string `json:"build_config,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
|
}
|
|
|
|
// TaskEnvironmentStatus represents the lifecycle state of a task execution environment.
|
|
type TaskEnvironmentStatus string
|
|
|
|
const (
|
|
TaskEnvironmentStatusCreating TaskEnvironmentStatus = "creating"
|
|
TaskEnvironmentStatusReady TaskEnvironmentStatus = "ready"
|
|
TaskEnvironmentStatusStopped TaskEnvironmentStatus = "stopped"
|
|
TaskEnvironmentStatusFailed TaskEnvironmentStatus = "failed"
|
|
)
|
|
|
|
// TaskEnvironment represents a per-task execution environment instance.
|
|
// It owns the workspace (worktree/container/sandbox) and the agentctl control server.
|
|
// Multiple sessions can share the same TaskEnvironment.
|
|
type TaskEnvironment struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
RepositoryID string `json:"repository_id"` // Deprecated: use Repos. Kept for dual-write/backwards compat.
|
|
ExecutorType string `json:"executor_type"`
|
|
ExecutorID string `json:"executor_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id"`
|
|
// AgentExecutionID was removed: executors_running owns the execution<->session
|
|
// mapping now. Read it via repo.GetExecutorRunningBySessionID(sessionID) when
|
|
// needed (the orchestrator does this in service_turns.go for WorkspaceInfo).
|
|
ControlPort int `json:"control_port"` // agentctl control port
|
|
Status TaskEnvironmentStatus `json:"status"`
|
|
|
|
// Type-specific fields. The single worktree fields below are legacy: with
|
|
// multi-repo tasks, the per-repo worktrees live on Repos. WorkspacePath
|
|
// continues to point at the agent workspace root (the task root when
|
|
// TaskDirName is set, otherwise the single repo's worktree path).
|
|
WorktreeID string `json:"worktree_id,omitempty"` // Deprecated: use Repos[i].WorktreeID
|
|
WorktreePath string `json:"worktree_path,omitempty"` // Deprecated: use Repos[i].WorktreePath
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"` // Deprecated: use Repos[i].WorktreeBranch
|
|
WorkspacePath string `json:"workspace_path,omitempty"`
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
SandboxID string `json:"sandbox_id,omitempty"`
|
|
|
|
// TaskDirName is the semantic directory name for the task (e.g. "fix-bug_ab12").
|
|
// Set when the task uses the multi-repo task-directory layout
|
|
// (~/.kandev/tasks/{TaskDirName}/{RepoName}/).
|
|
TaskDirName string `json:"task_dir_name,omitempty"`
|
|
|
|
// Repos contains one entry per repository associated with this environment.
|
|
// Populated by repository getters. Empty for environments created before the
|
|
// multi-repo backfill ran with no legacy repository_id.
|
|
Repos []*TaskEnvironmentRepo `json:"repos,omitempty"`
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// RepoFor returns the per-repo environment row for repositoryID, or nil if not present.
|
|
func (te *TaskEnvironment) RepoFor(repositoryID string) *TaskEnvironmentRepo {
|
|
for _, r := range te.Repos {
|
|
if r.RepositoryID == repositoryID {
|
|
return r
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TaskEnvironmentRepo represents the per-repository state of a task environment.
|
|
// One row per repository associated with the task: each carries its own worktree
|
|
// reference and any per-repo preparation error.
|
|
type TaskEnvironmentRepo struct {
|
|
ID string `json:"id"`
|
|
TaskEnvironmentID string `json:"task_environment_id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
WorktreeID string `json:"worktree_id,omitempty"`
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
Position int `json:"position"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ToAPI converts internal TaskEnvironment to API map.
|
|
func (te *TaskEnvironment) ToAPI() map[string]interface{} {
|
|
result := map[string]interface{}{
|
|
"id": te.ID,
|
|
"task_id": te.TaskID,
|
|
"repository_id": te.RepositoryID,
|
|
"executor_type": te.ExecutorType,
|
|
"executor_id": te.ExecutorID,
|
|
"executor_profile_id": te.ExecutorProfileID,
|
|
"status": string(te.Status),
|
|
"workspace_path": te.WorkspacePath,
|
|
"created_at": te.CreatedAt,
|
|
"updated_at": te.UpdatedAt,
|
|
}
|
|
// agent_execution_id is no longer carried on TaskEnvironment — see executors_running.
|
|
if te.ControlPort != 0 {
|
|
result["control_port"] = te.ControlPort
|
|
}
|
|
if te.WorktreeID != "" {
|
|
result["worktree_id"] = te.WorktreeID
|
|
}
|
|
if te.WorktreePath != "" {
|
|
result["worktree_path"] = te.WorktreePath
|
|
}
|
|
if te.WorktreeBranch != "" {
|
|
result["worktree_branch"] = te.WorktreeBranch
|
|
}
|
|
if te.ContainerID != "" {
|
|
result["container_id"] = te.ContainerID
|
|
}
|
|
if te.SandboxID != "" {
|
|
result["sandbox_id"] = te.SandboxID
|
|
}
|
|
if te.TaskDirName != "" {
|
|
result["task_dir_name"] = te.TaskDirName
|
|
}
|
|
if len(te.Repos) > 0 {
|
|
repos := make([]map[string]interface{}, 0, len(te.Repos))
|
|
for _, repo := range te.Repos {
|
|
repos = append(repos, repo.ToAPI())
|
|
}
|
|
result["repos"] = repos
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ToAPI converts internal TaskEnvironmentRepo to API map.
|
|
func (r *TaskEnvironmentRepo) ToAPI() map[string]interface{} {
|
|
out := map[string]interface{}{
|
|
"id": r.ID,
|
|
"task_environment_id": r.TaskEnvironmentID,
|
|
"repository_id": r.RepositoryID,
|
|
"position": r.Position,
|
|
"created_at": r.CreatedAt,
|
|
"updated_at": r.UpdatedAt,
|
|
}
|
|
if r.WorktreeID != "" {
|
|
out["worktree_id"] = r.WorktreeID
|
|
}
|
|
if r.WorktreePath != "" {
|
|
out["worktree_path"] = r.WorktreePath
|
|
}
|
|
if r.WorktreeBranch != "" {
|
|
out["worktree_branch"] = r.WorktreeBranch
|
|
}
|
|
if r.ErrorMessage != "" {
|
|
out["error_message"] = r.ErrorMessage
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TaskPlan represents a plan associated with a task
|
|
type TaskPlan struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
CreatedBy string `json:"created_by"` // "agent" or "user"
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// TaskPlanRevision is one immutable snapshot in the revision history of a task plan.
|
|
// Revisions are the source of truth for history; TaskPlan stores the latest revision's content as HEAD.
|
|
type TaskPlanRevision struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
RevisionNumber int `json:"revision_number"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
AuthorKind string `json:"author_kind"` // "agent" | "user"
|
|
AuthorName string `json:"author_name"` // display snapshot (agent profile name or user identifier)
|
|
RevertOfRevisionID *string `json:"revert_of_revision_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"` // bumps on coalesce merge
|
|
}
|
|
|
|
// TaskDocument represents a named document (plan, spec, notes, etc.) associated with a task.
|
|
// The key uniquely identifies the document within a task (e.g., "plan", "spec", "notes").
|
|
type TaskDocument struct {
|
|
ID string `json:"id" db:"id"`
|
|
TaskID string `json:"task_id" db:"task_id"`
|
|
Key string `json:"key" db:"key"`
|
|
Type string `json:"type" db:"type"`
|
|
Title string `json:"title" db:"title"`
|
|
Content string `json:"content" db:"content"`
|
|
AuthorKind string `json:"author_kind" db:"author_kind"`
|
|
AuthorName string `json:"author_name" db:"author_name"`
|
|
Filename string `json:"filename,omitempty" db:"filename"`
|
|
MimeType string `json:"mime_type,omitempty" db:"mime_type"`
|
|
SizeBytes int64 `json:"size_bytes,omitempty" db:"size_bytes"`
|
|
DiskPath string `json:"-" db:"disk_path"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// TaskDocumentRevision is one immutable snapshot in the revision history of a task document.
|
|
// Revisions are the source of truth for history; TaskDocument stores the latest revision's content as HEAD.
|
|
type TaskDocumentRevision struct {
|
|
ID string `json:"id" db:"id"`
|
|
TaskID string `json:"task_id" db:"task_id"`
|
|
DocumentKey string `json:"document_key" db:"document_key"`
|
|
RevisionNumber int `json:"revision_number" db:"revision_number"`
|
|
Title string `json:"title" db:"title"`
|
|
Content string `json:"content" db:"content"`
|
|
AuthorKind string `json:"author_kind" db:"author_kind"`
|
|
AuthorName string `json:"author_name" db:"author_name"`
|
|
RevertOfRevisionID *string `json:"revert_of_revision_id,omitempty" db:"revert_of_revision_id"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// SessionFileReview tracks per-file review state within a session
|
|
type SessionFileReview struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
FilePath string `json:"file_path"`
|
|
Reviewed bool `json:"reviewed"`
|
|
DiffHash string `json:"diff_hash"`
|
|
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ToAPI converts internal Task to API type
|
|
func (t *Task) ToAPI() *v1.Task {
|
|
// Convert TaskRepository models to API types
|
|
var repositories []v1.TaskRepository
|
|
for _, repo := range t.Repositories {
|
|
repositories = append(repositories, v1.TaskRepository{
|
|
ID: repo.ID,
|
|
TaskID: repo.TaskID,
|
|
RepositoryID: repo.RepositoryID,
|
|
BaseBranch: repo.BaseBranch,
|
|
Position: repo.Position,
|
|
Metadata: repo.Metadata,
|
|
CreatedAt: repo.CreatedAt,
|
|
UpdatedAt: repo.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
result := &v1.Task{
|
|
ID: t.ID,
|
|
WorkspaceID: t.WorkspaceID,
|
|
WorkflowID: t.WorkflowID,
|
|
Title: t.Title,
|
|
Description: t.Description,
|
|
State: t.State,
|
|
Priority: t.Priority,
|
|
Repositories: repositories,
|
|
CreatedAt: t.CreatedAt,
|
|
UpdatedAt: t.UpdatedAt,
|
|
Metadata: t.Metadata,
|
|
IsEphemeral: t.IsEphemeral,
|
|
ParentID: t.ParentID,
|
|
}
|
|
if t.Identifier != "" {
|
|
result.Identifier = t.Identifier
|
|
}
|
|
return result
|
|
}
|