2a1f7c2edf
* feat: sync workflows from a configured github repo Per-workspace setting pointing at a GitHub repo directory of portable workflow export files. A background poller fetches it on an interval and creates, updates, or deletes the matching workflows; a force-sync endpoint bypasses the content-hash check. Synced workflows carry source/source_path provenance and coexist with manual ones; changes that cannot be applied safely (steps or workflows that still hold tasks, ambiguous step names, unparseable files) surface as warnings in the new settings section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): fill workflow sync form from a pasted github link A new Repository link field parses plain repo URLs, SSH remotes, and tree/blob links (which also carry branch and directory) into the structured owner/repo/branch/path fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): simplify workflow sync form to link, branch, and interval The repository link is now the only repo input: owner, repo, and directory resolve from the pasted URL (echoed as a summary under the field), and a stored config renders back into a canonical tree link via buildGitHubRepoUrl. Branch and poll interval stay individually editable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): collapse workflow sync into a button, dialog, and status card The Workflows settings page now shows only a GitHub Sync button by default; the link/branch/interval form lives in a dialog that closes on successful save or removal. Once configured, a compact status card (mirroring the GitHub connection-status card) shows the synced repo, branch, directory, last result, warnings, and a Sync now action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): move github sync button into the workflows page header The GitHub Sync trigger now stacks under the Workspace settings button on the top right, and the extra separator around the sync area is gone — unconfigured workspaces show no sync chrome at all, configured ones show just the status card above the workflow list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): move github sync button next to the workflow actions GitHub Sync now sits in the Workflows section toolbar alongside Export All, Import, and Add Workflow; the status card renders inside the section above the workflow list. The toolbar moved to its own component to keep the page under the file-size limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): separate sync status card from the workflow list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): align sync status card with the workflow list gutter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair local edits to synced workflows on periodic sync Periodic syncs previously skipped applying when the repo content hash was unchanged, so local UI edits to a synced workflow were only reverted by a manual Sync now. Every sync now reconciles against the repo; the applier compares each step against its definition and only writes (and broadcasts) actual differences, so a no-drift sync stays silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: make github-synced workflows read-only Workflows with source=github reject UI mutations: workflow rename, description, agent-profile, and delete return 409 from the task-domain handlers; step create/update/delete/reorder and template application are guarded in the workflow controller. The sync applier writes through the service/repo layer and stays exempt. The frontend disables the card's inputs, save/delete, and the step pipeline editor for synced workflows, with a tooltip pointing at the synced repository. Drift repair on sync remains as the backstop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: optional workflow sync polling and repo-derived branch picker A per-workspace auto-sync toggle (poll_enabled, default on) controls the background polling loop; when off the workspace only syncs via Sync now. The dialog's branch field becomes a select fed by the repository's actual branches (same loader the task-create dialog uses), falling back to free text until branches load. Existing dev databases gain the new column via an idempotent ALTER. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: release synced workflows when the sync config is removed Deleting a workspace's workflow sync configuration converts its GitHub-sourced workflows back to manual ownership (source=manual), so they become editable instead of being stranded read-only. Release runs before the config row is deleted so a failure keeps the config for retry. The settings page reloads after removal (and after syncs that changed workflows) so cards unlock without a manual refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): drop branch selector and compact the auto-sync row The pasted link already carries the branch (main by default), so the dialog no longer needs a branch field — the resolved summary echoes it. The auto-sync toggle is now one compact row with the interval inline and a single short helper line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address PR review feedback Backend: fail-open read-only guard on workflow lookup errors (fixes MCP step handler CI failures); poller Stop holds its mutex through drain; per-workspace lock serializes syncs against config delete/replace; rollback workflow creation when provenance stamping fails; idiomatic duplicate-column migration via db.IsDuplicateColumnError; branch-name validation and a 30-day interval cap; generic 500s with logged detail; WS read-only rejections use CONFLICT with the real reason; e2e reset aborts when sync-config cleanup fails; test hygiene (t.Cleanup, SetMaxOpenConns). Frontend: malformed percent escapes parse as invalid instead of throwing; save disabled during initial load and for invalid intervals; Remove disabled while saving; stale async config responses discarded; delete-dialog confirms re-check read-only; focusable Synced badge; explicit button types; failed polls labeled 'last attempt'; unique warning list keys; test cleanup via afterEach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: guard workflow sync test fake against concurrent poller access The poller invokes the fake applier from its own goroutine while the synctest test asserts on call counts; -race caught the unguarded reads. Also documents that the per-workspace lock is deliberately held across the GitHub fetch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
850 lines
35 KiB
Go
850 lines
35 KiB
Go
package dto
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
"github.com/kandev/kandev/internal/task/service"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
)
|
|
|
|
type WorkflowDTO struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
SortOrder int `json:"sort_order"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
// Style is a Phase 2 (ADR-0004) UX hint read by the frontend ONLY.
|
|
// Allowed values: "kanban" | "office" | "custom".
|
|
Style string `json:"style,omitempty"`
|
|
// Source records where the workflow definition came from ("manual" |
|
|
// "github"); SourcePath is the repo-relative file for synced workflows.
|
|
Source string `json:"source,omitempty"`
|
|
SourcePath string `json:"source_path,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type WorkspaceDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
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"`
|
|
TaskPrefix string `json:"task_prefix,omitempty"`
|
|
TaskSequence int `json:"task_sequence,omitempty"`
|
|
OfficeWorkflowID string `json:"office_workflow_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type RepositoryDTO struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
SourceType string `json:"source_type"`
|
|
LocalPath string `json:"local_path"`
|
|
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"`
|
|
WorktreeBranchTemplate string `json:"worktree_branch_template"`
|
|
PullBeforeWorktree bool `json:"pull_before_worktree"`
|
|
SetupScript string `json:"setup_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
DevScript string `json:"dev_script"`
|
|
CopyFiles string `json:"copy_files"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Scripts []RepositoryScriptDTO `json:"scripts,omitempty"`
|
|
}
|
|
|
|
type RepositoryScriptDTO 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"`
|
|
}
|
|
|
|
type ExecutorDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type models.ExecutorType `json:"type"`
|
|
Status models.ExecutorStatus `json:"status"`
|
|
IsSystem bool `json:"is_system"`
|
|
Resumable bool `json:"resumable"`
|
|
Config map[string]string `json:"config,omitempty"`
|
|
Profiles []ExecutorProfileDTO `json:"profiles,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ExecutorProfileDTO struct {
|
|
ID string `json:"id"`
|
|
ExecutorID string `json:"executor_id"`
|
|
ExecutorType string `json:"executor_type,omitempty"`
|
|
ExecutorName string `json:"executor_name,omitempty"`
|
|
Name string `json:"name"`
|
|
McpPolicy string `json:"mcp_policy,omitempty"`
|
|
Config map[string]string `json:"config,omitempty"`
|
|
PrepareScript string `json:"prepare_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
EnvVars []models.ProfileEnvVar `json:"env_vars,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ListExecutorProfilesResponse struct {
|
|
Profiles []ExecutorProfileDTO `json:"profiles"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type EnvironmentDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Kind models.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"`
|
|
}
|
|
|
|
type TaskDTO 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"`
|
|
Repositories []TaskRepositoryDTO `json:"repositories,omitempty"`
|
|
Position int `json:"position"`
|
|
PrimarySessionID *string `json:"primary_session_id,omitempty"`
|
|
SessionCount *int `json:"session_count,omitempty"`
|
|
ReviewStatus models.ReviewStatus `json:"review_status,omitempty"`
|
|
PrimaryExecutorID *string `json:"primary_executor_id,omitempty"`
|
|
PrimaryExecutorType *string `json:"primary_executor_type,omitempty"`
|
|
PrimaryExecutorName *string `json:"primary_executor_name,omitempty"`
|
|
PrimaryAgentName *string `json:"primary_agent_name,omitempty"`
|
|
PrimaryWorkingDirectory *string `json:"primary_working_directory,omitempty"`
|
|
PrimarySessionState *string `json:"primary_session_state,omitempty"`
|
|
PrimarySessionPendingAction *string `json:"primary_session_pending_action"`
|
|
IsRemoteExecutor bool `json:"is_remote_executor,omitempty"`
|
|
ParentID string `json:"parent_id,omitempty"`
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
|
|
// Office extensions
|
|
AssigneeAgentProfileID string `json:"assignee_agent_profile_id,omitempty"`
|
|
Origin string `json:"origin,omitempty"`
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
Labels string `json:"labels,omitempty"`
|
|
Identifier string `json:"identifier,omitempty"`
|
|
// IsFromOffice is the authoritative "this task is owned by office"
|
|
// flag. Computed by the task repo at read time as
|
|
// (project_id != '' OR workflow_id == workspace.office_workflow_id).
|
|
// True for office tasks even when they have no project yet; false for
|
|
// kanban-board tasks. Use this to gate office-only UI (e.g. the
|
|
// "Open in office view" topbar link).
|
|
IsFromOffice bool `json:"is_from_office,omitempty"`
|
|
|
|
// PRs lists the GitHub pull requests associated with this task, when the
|
|
// github service is wired and any association exists. Surfaced through the
|
|
// task-listing MCP tools so agents can reason about PR status (e.g. find
|
|
// tasks whose PRs are merged). Omitted when empty.
|
|
PRs []v1.TaskPRSummary `json:"prs,omitempty"`
|
|
}
|
|
|
|
type TaskRepositoryDTO 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"`
|
|
}
|
|
|
|
type TaskSessionDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
AgentExecutionID string `json:"agent_execution_id,omitempty"`
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
ExecutorID string `json:"executor_id,omitempty"`
|
|
ExecutorProfileID string `json:"executor_profile_id,omitempty"`
|
|
EnvironmentID string `json:"environment_id,omitempty"`
|
|
RepositoryID string `json:"repository_id,omitempty"`
|
|
BaseBranch string `json:"base_branch,omitempty"`
|
|
BaseCommitSHA string `json:"base_commit_sha,omitempty"`
|
|
WorktreeID string `json:"worktree_id,omitempty"`
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
State models.TaskSessionState `json:"state"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
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"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
// Workflow fields
|
|
IsPrimary bool `json:"is_primary"`
|
|
IsPassthrough bool `json:"is_passthrough"`
|
|
ReviewStatus models.ReviewStatus `json:"review_status,omitempty"`
|
|
TaskEnvironmentID string `json:"task_environment_id,omitempty"`
|
|
}
|
|
|
|
// TaskSessionSummaryDTO is a lightweight version of TaskSessionDTO without snapshot fields.
|
|
// Used for list endpoints where snapshots are not needed, reducing response size by ~40-60%.
|
|
type TaskSessionSummaryDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
AgentExecutionID string `json:"agent_execution_id,omitempty"`
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
ExecutorID string `json:"executor_id,omitempty"`
|
|
ExecutorProfileID string `json:"executor_profile_id,omitempty"`
|
|
EnvironmentID string `json:"environment_id,omitempty"`
|
|
RepositoryID string `json:"repository_id,omitempty"`
|
|
BaseBranch string `json:"base_branch,omitempty"`
|
|
BaseCommitSHA string `json:"base_commit_sha,omitempty"`
|
|
WorktreeID string `json:"worktree_id,omitempty"`
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
State models.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"`
|
|
IsPrimary bool `json:"is_primary"`
|
|
IsPassthrough bool `json:"is_passthrough"`
|
|
ReviewStatus models.ReviewStatus `json:"review_status,omitempty"`
|
|
TaskEnvironmentID string `json:"task_environment_id,omitempty"`
|
|
// CommandCount is the number of tool_call messages on this session,
|
|
// surfaced inline in the timeline entry header ("ran N commands").
|
|
// Populated by ListTaskSessions; defaults to 0 for callers that don't
|
|
// resolve it.
|
|
CommandCount int `json:"command_count"`
|
|
}
|
|
|
|
// ListTaskSessionSummariesResponse is the list response using summary DTOs.
|
|
type ListTaskSessionSummariesResponse struct {
|
|
Sessions []TaskSessionSummaryDTO `json:"sessions"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type GetTaskSessionResponse struct {
|
|
Session TaskSessionDTO `json:"session"`
|
|
}
|
|
|
|
type ListTaskSessionsResponse struct {
|
|
Sessions []TaskSessionDTO `json:"sessions"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type WorkflowSnapshotDTO struct {
|
|
Workflow WorkflowDTO `json:"workflow"`
|
|
Steps []WorkflowStepDTO `json:"steps"`
|
|
Tasks []TaskDTO `json:"tasks"`
|
|
}
|
|
|
|
type ListMessagesResponse struct {
|
|
Messages []*v1.Message `json:"messages"`
|
|
Total int `json:"total"`
|
|
HasMore bool `json:"has_more"`
|
|
Cursor string `json:"cursor"`
|
|
}
|
|
|
|
// MessageSearchHit is a lightweight match returned by a session message search.
|
|
type MessageSearchHit struct {
|
|
ID string `json:"id"`
|
|
TurnID string `json:"turn_id,omitempty"`
|
|
AuthorType string `json:"author_type"`
|
|
Type string `json:"type"`
|
|
Snippet string `json:"snippet"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// SearchMessagesResponse contains hits from a session message search.
|
|
type SearchMessagesResponse struct {
|
|
Hits []MessageSearchHit `json:"hits"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type TurnDTO struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
TaskID string `json:"task_id"`
|
|
StartedAt string `json:"started_at"`
|
|
CompletedAt *string `json:"completed_at,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type ListTurnsResponse struct {
|
|
Turns []TurnDTO `json:"turns"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListWorkflowsResponse struct {
|
|
Workflows []WorkflowDTO `json:"workflows"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListWorkspacesResponse struct {
|
|
Workspaces []WorkspaceDTO `json:"workspaces"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListRepositoriesResponse struct {
|
|
Repositories []RepositoryDTO `json:"repositories"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListRepositoryScriptsResponse struct {
|
|
Scripts []RepositoryScriptDTO `json:"scripts"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListExecutorsResponse struct {
|
|
Executors []ExecutorDTO `json:"executors"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListEnvironmentsResponse struct {
|
|
Environments []EnvironmentDTO `json:"environments"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type BranchDTO struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"` // "local" or "remote"
|
|
Remote string `json:"remote"` // remote name (e.g., "origin") for remote branches
|
|
}
|
|
|
|
type RepositoryBranchesResponse struct {
|
|
Branches []BranchDTO `json:"branches"`
|
|
Total int `json:"total"`
|
|
CurrentBranch string `json:"current_branch,omitempty"`
|
|
// FetchedAt is the timestamp of the most recent `git fetch` for this
|
|
// repository (RFC3339). Empty when no refresh has been requested in the
|
|
// process lifetime.
|
|
FetchedAt string `json:"fetched_at,omitempty"`
|
|
// FetchError is the human-readable error from the last fetch attempt for
|
|
// this request, if one was attempted and failed. Empty otherwise.
|
|
FetchError string `json:"fetch_error,omitempty"`
|
|
}
|
|
|
|
// LocalRepositoryStatusResponse reports current branch + dirty paths for a
|
|
// local repository on disk (no session required). Used by the task-create
|
|
// dialog to preflight the fresh-branch flow on local executors.
|
|
type LocalRepositoryStatusResponse struct {
|
|
CurrentBranch string `json:"current_branch"`
|
|
DirtyFiles []string `json:"dirty_files"`
|
|
}
|
|
|
|
type LocalRepositoryDTO struct {
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
DefaultBranch string `json:"default_branch,omitempty"`
|
|
}
|
|
|
|
type RepositoryDiscoveryResponse struct {
|
|
Roots []string `json:"roots"`
|
|
Repositories []LocalRepositoryDTO `json:"repositories"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type RepositoryPathValidationResponse struct {
|
|
Path string `json:"path"`
|
|
Exists bool `json:"exists"`
|
|
IsGitRepo bool `json:"is_git"`
|
|
Allowed bool `json:"allowed"`
|
|
DefaultBranch string `json:"default_branch,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type ListTasksResponse struct {
|
|
Tasks []TaskDTO `json:"tasks"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type SuccessResponse struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
func FromWorkflow(workflow *models.Workflow) WorkflowDTO {
|
|
var description *string
|
|
if workflow.Description != "" {
|
|
description = &workflow.Description
|
|
}
|
|
|
|
return WorkflowDTO{
|
|
ID: workflow.ID,
|
|
WorkspaceID: workflow.WorkspaceID,
|
|
Name: workflow.Name,
|
|
Description: description,
|
|
AgentProfileID: workflow.AgentProfileID,
|
|
SortOrder: workflow.SortOrder,
|
|
Hidden: workflow.Hidden,
|
|
Style: workflow.Style,
|
|
Source: workflow.Source,
|
|
SourcePath: workflow.SourcePath,
|
|
CreatedAt: workflow.CreatedAt,
|
|
UpdatedAt: workflow.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromWorkspace(workspace *models.Workspace) WorkspaceDTO {
|
|
var description *string
|
|
if workspace.Description != "" {
|
|
description = &workspace.Description
|
|
}
|
|
|
|
return WorkspaceDTO{
|
|
ID: workspace.ID,
|
|
Name: workspace.Name,
|
|
Description: description,
|
|
OwnerID: workspace.OwnerID,
|
|
DefaultExecutorID: workspace.DefaultExecutorID,
|
|
DefaultEnvironmentID: workspace.DefaultEnvironmentID,
|
|
DefaultAgentProfileID: workspace.DefaultAgentProfileID,
|
|
DefaultConfigAgentProfileID: workspace.DefaultConfigAgentProfileID,
|
|
TaskPrefix: workspace.TaskPrefix,
|
|
TaskSequence: workspace.TaskSequence,
|
|
OfficeWorkflowID: workspace.OfficeWorkflowID,
|
|
CreatedAt: workspace.CreatedAt,
|
|
UpdatedAt: workspace.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromRepository(repository *models.Repository) RepositoryDTO {
|
|
return RepositoryDTO{
|
|
ID: repository.ID,
|
|
WorkspaceID: repository.WorkspaceID,
|
|
Name: repository.Name,
|
|
SourceType: repository.SourceType,
|
|
LocalPath: repository.LocalPath,
|
|
Provider: repository.Provider,
|
|
ProviderRepoID: repository.ProviderRepoID,
|
|
ProviderOwner: repository.ProviderOwner,
|
|
ProviderName: repository.ProviderName,
|
|
DefaultBranch: repository.DefaultBranch,
|
|
WorktreeBranchPrefix: repository.WorktreeBranchPrefix,
|
|
WorktreeBranchTemplate: repository.WorktreeBranchTemplate,
|
|
PullBeforeWorktree: repository.PullBeforeWorktree,
|
|
SetupScript: repository.SetupScript,
|
|
CleanupScript: repository.CleanupScript,
|
|
DevScript: repository.DevScript,
|
|
CopyFiles: repository.CopyFiles,
|
|
CreatedAt: repository.CreatedAt,
|
|
UpdatedAt: repository.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromRepositoryScript(script *models.RepositoryScript) RepositoryScriptDTO {
|
|
return RepositoryScriptDTO{
|
|
ID: script.ID,
|
|
RepositoryID: script.RepositoryID,
|
|
Name: script.Name,
|
|
Command: script.Command,
|
|
Position: script.Position,
|
|
CreatedAt: script.CreatedAt,
|
|
UpdatedAt: script.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromExecutor(executor *models.Executor) ExecutorDTO {
|
|
return ExecutorDTO{
|
|
ID: executor.ID,
|
|
Name: executor.Name,
|
|
Type: executor.Type,
|
|
Status: executor.Status,
|
|
IsSystem: executor.IsSystem,
|
|
Resumable: executor.Resumable,
|
|
Config: executor.Config,
|
|
CreatedAt: executor.CreatedAt,
|
|
UpdatedAt: executor.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromExecutorProfile(profile *models.ExecutorProfile) ExecutorProfileDTO {
|
|
return ExecutorProfileDTO{
|
|
ID: profile.ID,
|
|
ExecutorID: profile.ExecutorID,
|
|
Name: profile.Name,
|
|
McpPolicy: profile.McpPolicy,
|
|
Config: profile.Config,
|
|
PrepareScript: profile.PrepareScript,
|
|
CleanupScript: profile.CleanupScript,
|
|
EnvVars: profile.EnvVars,
|
|
CreatedAt: profile.CreatedAt,
|
|
UpdatedAt: profile.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// FromExecutorProfileWithExecutor converts an ExecutorProfile model to a DTO
|
|
// with executor type and name populated.
|
|
func FromExecutorProfileWithExecutor(profile *models.ExecutorProfile, executor *models.Executor) ExecutorProfileDTO {
|
|
d := FromExecutorProfile(profile)
|
|
if executor != nil {
|
|
d.ExecutorType = string(executor.Type)
|
|
d.ExecutorName = executor.Name
|
|
}
|
|
return d
|
|
}
|
|
|
|
func FromEnvironment(environment *models.Environment) EnvironmentDTO {
|
|
return EnvironmentDTO{
|
|
ID: environment.ID,
|
|
Name: environment.Name,
|
|
Kind: environment.Kind,
|
|
IsSystem: environment.IsSystem,
|
|
WorktreeRoot: environment.WorktreeRoot,
|
|
ImageTag: environment.ImageTag,
|
|
Dockerfile: environment.Dockerfile,
|
|
BuildConfig: environment.BuildConfig,
|
|
CreatedAt: environment.CreatedAt,
|
|
UpdatedAt: environment.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromLocalRepository(repo service.LocalRepository) LocalRepositoryDTO {
|
|
return LocalRepositoryDTO{
|
|
Path: repo.Path,
|
|
Name: repo.Name,
|
|
DefaultBranch: repo.DefaultBranch,
|
|
}
|
|
}
|
|
|
|
func FromTask(task *models.Task) TaskDTO {
|
|
return FromTaskWithPrimarySession(task, nil)
|
|
}
|
|
|
|
// FromTaskWithPrimarySession converts a task model to a TaskDTO, including the primary session ID.
|
|
func FromTaskWithPrimarySession(task *models.Task, primarySessionID *string) TaskDTO {
|
|
return FromTaskWithSessionInfo(task, primarySessionID, nil, models.ReviewStatusNone, nil, nil, nil, nil, nil, nil, nil)
|
|
}
|
|
|
|
// FromTaskWithSessionInfo converts a task model to a TaskDTO, including session information.
|
|
func FromTaskWithSessionInfo(
|
|
task *models.Task,
|
|
primarySessionID *string,
|
|
sessionCount *int,
|
|
reviewStatus models.ReviewStatus,
|
|
primaryExecutorID *string,
|
|
primaryExecutorType *string,
|
|
primaryExecutorName *string,
|
|
primaryAgentName *string,
|
|
primaryWorkingDirectory *string,
|
|
primarySessionState *string,
|
|
primarySessionPendingAction *string,
|
|
) TaskDTO {
|
|
// Convert repositories
|
|
var repositories []TaskRepositoryDTO
|
|
for _, repo := range task.Repositories {
|
|
repositories = append(repositories, TaskRepositoryDTO{
|
|
ID: repo.ID,
|
|
TaskID: repo.TaskID,
|
|
RepositoryID: repo.RepositoryID,
|
|
BaseBranch: repo.BaseBranch,
|
|
CheckoutBranch: repo.CheckoutBranch,
|
|
Position: repo.Position,
|
|
Metadata: repo.Metadata,
|
|
CreatedAt: repo.CreatedAt,
|
|
UpdatedAt: repo.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
return TaskDTO{
|
|
ID: task.ID,
|
|
WorkspaceID: task.WorkspaceID,
|
|
WorkflowID: task.WorkflowID,
|
|
WorkflowStepID: task.WorkflowStepID,
|
|
Title: task.Title,
|
|
Description: task.Description,
|
|
State: task.State,
|
|
Priority: task.Priority,
|
|
Repositories: repositories,
|
|
Position: task.Position,
|
|
PrimarySessionID: primarySessionID,
|
|
SessionCount: sessionCount,
|
|
ReviewStatus: reviewStatus,
|
|
PrimaryExecutorID: primaryExecutorID,
|
|
PrimaryExecutorType: primaryExecutorType,
|
|
PrimaryExecutorName: primaryExecutorName,
|
|
PrimaryAgentName: primaryAgentName,
|
|
PrimaryWorkingDirectory: primaryWorkingDirectory,
|
|
PrimarySessionState: primarySessionState,
|
|
PrimarySessionPendingAction: primarySessionPendingAction,
|
|
IsRemoteExecutor: primaryExecutorType != nil && models.IsRemoteExecutorType(models.ExecutorType(*primaryExecutorType)),
|
|
ParentID: task.ParentID,
|
|
ArchivedAt: task.ArchivedAt,
|
|
CreatedAt: task.CreatedAt,
|
|
UpdatedAt: task.UpdatedAt,
|
|
Metadata: task.Metadata,
|
|
// Office extensions. AssigneeAgentProfileID is a read-time
|
|
// projection from workflow_step_participants (ADR 0005 Wave F);
|
|
// the repo's task SELECTs hydrate it via a correlated subquery.
|
|
AssigneeAgentProfileID: task.AssigneeAgentProfileID,
|
|
Origin: task.Origin,
|
|
ProjectID: task.ProjectID,
|
|
Labels: task.Labels,
|
|
Identifier: task.Identifier,
|
|
IsFromOffice: task.IsFromOffice,
|
|
}
|
|
}
|
|
|
|
// FromTaskSessionSummary converts a session model to a summary DTO (no snapshot fields).
|
|
func FromTaskSessionSummary(session *models.TaskSession) TaskSessionSummaryDTO {
|
|
result := TaskSessionSummaryDTO{
|
|
ID: session.ID,
|
|
TaskID: session.TaskID,
|
|
AgentExecutionID: session.AgentExecutionID,
|
|
ContainerID: session.ContainerID,
|
|
AgentProfileID: session.AgentProfileID,
|
|
ExecutorID: session.ExecutorID,
|
|
ExecutorProfileID: session.ExecutorProfileID,
|
|
EnvironmentID: session.EnvironmentID,
|
|
RepositoryID: session.RepositoryID,
|
|
BaseBranch: session.BaseBranch,
|
|
BaseCommitSHA: session.BaseCommitSHA,
|
|
State: session.State,
|
|
ErrorMessage: session.ErrorMessage,
|
|
Metadata: session.Metadata,
|
|
StartedAt: session.StartedAt,
|
|
CompletedAt: session.CompletedAt,
|
|
UpdatedAt: session.UpdatedAt,
|
|
IsPrimary: session.IsPrimary,
|
|
IsPassthrough: session.IsPassthrough,
|
|
ReviewStatus: session.ReviewStatus,
|
|
TaskEnvironmentID: session.TaskEnvironmentID,
|
|
}
|
|
if len(session.Worktrees) > 0 {
|
|
result.WorktreeID = session.Worktrees[0].WorktreeID
|
|
result.WorktreePath = session.Worktrees[0].WorktreePath
|
|
result.WorktreeBranch = session.Worktrees[0].WorktreeBranch
|
|
}
|
|
return result
|
|
}
|
|
|
|
func FromTaskSession(session *models.TaskSession) TaskSessionDTO {
|
|
result := TaskSessionDTO{
|
|
ID: session.ID,
|
|
TaskID: session.TaskID,
|
|
AgentExecutionID: session.AgentExecutionID,
|
|
ContainerID: session.ContainerID,
|
|
AgentProfileID: session.AgentProfileID,
|
|
ExecutorID: session.ExecutorID,
|
|
ExecutorProfileID: session.ExecutorProfileID,
|
|
EnvironmentID: session.EnvironmentID,
|
|
RepositoryID: session.RepositoryID,
|
|
BaseBranch: session.BaseBranch,
|
|
BaseCommitSHA: session.BaseCommitSHA,
|
|
State: session.State,
|
|
ErrorMessage: session.ErrorMessage,
|
|
Metadata: session.Metadata,
|
|
AgentProfileSnapshot: session.AgentProfileSnapshot,
|
|
ExecutorSnapshot: session.ExecutorSnapshot,
|
|
EnvironmentSnapshot: session.EnvironmentSnapshot,
|
|
RepositorySnapshot: session.RepositorySnapshot,
|
|
StartedAt: session.StartedAt,
|
|
CompletedAt: session.CompletedAt,
|
|
UpdatedAt: session.UpdatedAt,
|
|
// Workflow fields
|
|
IsPrimary: session.IsPrimary,
|
|
IsPassthrough: session.IsPassthrough,
|
|
ReviewStatus: session.ReviewStatus,
|
|
TaskEnvironmentID: session.TaskEnvironmentID,
|
|
}
|
|
if len(session.Worktrees) > 0 {
|
|
result.WorktreeID = session.Worktrees[0].WorktreeID
|
|
result.WorktreePath = session.Worktrees[0].WorktreePath
|
|
result.WorktreeBranch = session.Worktrees[0].WorktreeBranch
|
|
}
|
|
return result
|
|
}
|
|
|
|
// WorkflowStepDTO represents a workflow step for API responses
|
|
type WorkflowStepDTO struct {
|
|
ID string `json:"id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
Name string `json:"name"`
|
|
Position int `json:"position"`
|
|
Color string `json:"color"`
|
|
Prompt string `json:"prompt,omitempty"`
|
|
Events *StepEventsDTO `json:"events,omitempty"`
|
|
AllowManualMove bool `json:"allow_manual_move"`
|
|
IsStartStep bool `json:"is_start_step"`
|
|
ShowInCommandPanel bool `json:"show_in_command_panel"`
|
|
AutoArchiveAfterHours int `json:"auto_archive_after_hours,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
WIPLimit int `json:"wip_limit"`
|
|
PullFromStepID string `json:"pull_from_step_id,omitempty"`
|
|
// StageType is a Phase 2 (ADR-0004) semantic hint for the frontend.
|
|
// Allowed values: "work" | "review" | "approval" | "custom".
|
|
StageType string `json:"stage_type,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// StepEventsDTO represents step events for API responses
|
|
type StepEventsDTO struct {
|
|
OnEnter []StepActionDTO `json:"on_enter,omitempty"`
|
|
OnTurnStart []StepActionDTO `json:"on_turn_start,omitempty"`
|
|
OnTurnComplete []StepActionDTO `json:"on_turn_complete,omitempty"`
|
|
OnExit []StepActionDTO `json:"on_exit,omitempty"`
|
|
OnComment []StepActionDTO `json:"on_comment,omitempty"`
|
|
OnBlockerResolved []StepActionDTO `json:"on_blocker_resolved,omitempty"`
|
|
OnChildrenCompleted []StepActionDTO `json:"on_children_completed,omitempty"`
|
|
OnApprovalResolved []StepActionDTO `json:"on_approval_resolved,omitempty"`
|
|
OnHeartbeat []StepActionDTO `json:"on_heartbeat,omitempty"`
|
|
OnBudgetAlert []StepActionDTO `json:"on_budget_alert,omitempty"`
|
|
OnAgentError []StepActionDTO `json:"on_agent_error,omitempty"`
|
|
}
|
|
|
|
// StepActionDTO represents a step action for API responses
|
|
type StepActionDTO struct {
|
|
Type string `json:"type"`
|
|
Config map[string]interface{} `json:"config,omitempty"`
|
|
}
|
|
|
|
// MoveTaskResponse includes the task and the target workflow step info
|
|
type MoveTaskResponse struct {
|
|
Task TaskDTO `json:"task"`
|
|
WorkflowStep WorkflowStepDTO `json:"workflow_step"`
|
|
}
|
|
|
|
// Session Workflow Review DTOs
|
|
|
|
// ApproveSessionRequest is the request to approve a session's current step
|
|
type ApproveSessionRequest struct {
|
|
SessionID string `json:"-"` // From URL path
|
|
}
|
|
|
|
// ApproveSessionResponse is the response after approving a session
|
|
type ApproveSessionResponse struct {
|
|
Success bool `json:"success"`
|
|
Session TaskSessionDTO `json:"session"`
|
|
WorkflowStep WorkflowStepDTO `json:"workflow_step,omitempty"` // New step after approval
|
|
}
|
|
|
|
// TaskPlanDTO represents a task plan for API responses
|
|
type TaskPlanDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
CreatedBy string `json:"created_by"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
ImplementationStartedAt *time.Time `json:"implementation_started_at,omitempty"`
|
|
ImplementationStartedSessionID *string `json:"implementation_started_session_id,omitempty"`
|
|
ImplementationStartedBy *string `json:"implementation_started_by,omitempty"`
|
|
}
|
|
|
|
// TaskPlanFromModel converts a TaskPlan model to a TaskPlanDTO.
|
|
func TaskPlanFromModel(plan *models.TaskPlan) *TaskPlanDTO {
|
|
if plan == nil {
|
|
return nil
|
|
}
|
|
return &TaskPlanDTO{
|
|
ID: plan.ID,
|
|
TaskID: plan.TaskID,
|
|
Title: plan.Title,
|
|
Content: plan.Content,
|
|
CreatedBy: plan.CreatedBy,
|
|
CreatedAt: plan.CreatedAt,
|
|
UpdatedAt: plan.UpdatedAt,
|
|
ImplementationStartedAt: plan.ImplementationStartedAt,
|
|
ImplementationStartedSessionID: plan.ImplementationStartedSessionID,
|
|
ImplementationStartedBy: plan.ImplementationStartedBy,
|
|
}
|
|
}
|
|
|
|
// TaskPlanRevisionDTO represents a plan revision for API responses.
|
|
// Content is optional so list responses can omit it (fetched on demand).
|
|
type TaskPlanRevisionDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
RevisionNumber int `json:"revision_number"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content,omitempty"`
|
|
AuthorKind string `json:"author_kind"`
|
|
AuthorName string `json:"author_name"`
|
|
RevertOfRevisionID *string `json:"revert_of_revision_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// TaskPlanRevisionFromModel converts a TaskPlanRevision model with content.
|
|
func TaskPlanRevisionFromModel(rev *models.TaskPlanRevision) *TaskPlanRevisionDTO {
|
|
if rev == nil {
|
|
return nil
|
|
}
|
|
return &TaskPlanRevisionDTO{
|
|
ID: rev.ID,
|
|
TaskID: rev.TaskID,
|
|
RevisionNumber: rev.RevisionNumber,
|
|
Title: rev.Title,
|
|
Content: rev.Content,
|
|
AuthorKind: rev.AuthorKind,
|
|
AuthorName: rev.AuthorName,
|
|
RevertOfRevisionID: rev.RevertOfRevisionID,
|
|
CreatedAt: rev.CreatedAt,
|
|
UpdatedAt: rev.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// TaskPlanRevisionMetaFromModel converts without content (for list payloads/WS broadcasts).
|
|
func TaskPlanRevisionMetaFromModel(rev *models.TaskPlanRevision) *TaskPlanRevisionDTO {
|
|
meta := TaskPlanRevisionFromModel(rev)
|
|
if meta != nil {
|
|
meta.Content = ""
|
|
}
|
|
return meta
|
|
}
|
|
|
|
// FromTurn converts a Turn model to a TurnDTO.
|
|
func FromTurn(turn *models.Turn) TurnDTO {
|
|
var completedAt *string
|
|
if turn.CompletedAt != nil {
|
|
formatted := turn.CompletedAt.UTC().Format(time.RFC3339)
|
|
completedAt = &formatted
|
|
}
|
|
|
|
return TurnDTO{
|
|
ID: turn.ID,
|
|
SessionID: turn.TaskSessionID,
|
|
TaskID: turn.TaskID,
|
|
StartedAt: turn.StartedAt.UTC().Format(time.RFC3339),
|
|
CompletedAt: completedAt,
|
|
Metadata: turn.Metadata,
|
|
CreatedAt: turn.CreatedAt.UTC().Format(time.RFC3339),
|
|
UpdatedAt: turn.UpdatedAt.UTC().Format(time.RFC3339),
|
|
}
|
|
}
|