314 lines
10 KiB
Go
314 lines
10 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/kandev/kandev/internal/github"
|
|
officeconfig "github.com/kandev/kandev/internal/office/config"
|
|
"github.com/kandev/kandev/internal/office/configloader"
|
|
officedashboard "github.com/kandev/kandev/internal/office/dashboard"
|
|
officeengineadapters "github.com/kandev/kandev/internal/office/engine_adapters"
|
|
officeonboarding "github.com/kandev/kandev/internal/office/onboarding"
|
|
officesqlite "github.com/kandev/kandev/internal/office/repository/sqlite"
|
|
officeroutines "github.com/kandev/kandev/internal/office/routines"
|
|
officeservice "github.com/kandev/kandev/internal/office/service"
|
|
officewakeup "github.com/kandev/kandev/internal/office/wakeup"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
tasksqlite "github.com/kandev/kandev/internal/task/repository/sqlite"
|
|
taskservice "github.com/kandev/kandev/internal/task/service"
|
|
)
|
|
|
|
// taskWorkspaceCreatorAdapter adapts the task service to the office
|
|
// WorkspaceCreator interface for dual workspace creation.
|
|
type taskWorkspaceCreatorAdapter struct {
|
|
taskSvc *taskservice.Service
|
|
}
|
|
|
|
func (a *taskWorkspaceCreatorAdapter) CreateWorkspace(ctx context.Context, name, description string) error {
|
|
_, err := a.taskSvc.CreateWorkspace(ctx, &taskservice.CreateWorkspaceRequest{
|
|
Name: name,
|
|
Description: description,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (a *taskWorkspaceCreatorAdapter) FindWorkspaceIDByName(ctx context.Context, name string) (string, error) {
|
|
workspaces, err := a.taskSvc.ListWorkspaces(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, ws := range workspaces {
|
|
if ws.Name == name {
|
|
return ws.ID, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (a *taskWorkspaceCreatorAdapter) ListWorkspaceNames(ctx context.Context) ([]string, error) {
|
|
workspaces, err := a.taskSvc.ListWorkspaces(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := make([]string, len(workspaces))
|
|
for i, ws := range workspaces {
|
|
names[i] = ws.Name
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// taskPRListerAdapter adapts *github.Service to the office TaskPRLister
|
|
// interface so the children-completed payload can carry per-child PR URLs.
|
|
// Returns an empty map (no PRs) when the github service is nil.
|
|
type taskPRListerAdapter struct {
|
|
gh *github.Service
|
|
}
|
|
|
|
func (a *taskPRListerAdapter) ListTaskPRsByTaskIDs(
|
|
ctx context.Context, taskIDs []string,
|
|
) (map[string][]officeservice.TaskPRLink, error) {
|
|
out := make(map[string][]officeservice.TaskPRLink)
|
|
if a.gh == nil || len(taskIDs) == 0 {
|
|
return out, nil
|
|
}
|
|
prs, err := a.gh.ListTaskPRsByTaskIDs(ctx, taskIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for taskID, list := range prs {
|
|
links := make([]officeservice.TaskPRLink, 0, len(list))
|
|
for _, pr := range list {
|
|
if pr == nil {
|
|
continue
|
|
}
|
|
links = append(links, officeservice.TaskPRLink{
|
|
URL: pr.PRURL,
|
|
Title: pr.PRTitle,
|
|
Number: pr.PRNumber,
|
|
State: pr.State,
|
|
})
|
|
}
|
|
if len(links) > 0 {
|
|
out[taskID] = links
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// childTaskCreatorAdapter bridges *taskservice.Service to the
|
|
// engine_adapters.ChildTaskCreator interface. The two structs use
|
|
// identical fields but live in different packages — this adapter copies
|
|
// the fields field-by-field to keep the dependency graph acyclic.
|
|
type childTaskCreatorAdapter struct {
|
|
taskSvc *taskservice.Service
|
|
}
|
|
|
|
func (a *childTaskCreatorAdapter) CreateChildTask(
|
|
ctx context.Context, parent *models.Task, spec officeengineadapters.ChildTaskCreateSpec,
|
|
) (string, error) {
|
|
return a.taskSvc.CreateChildTask(ctx, parent, taskservice.ChildTaskSpec{
|
|
Title: spec.Title,
|
|
Description: spec.Description,
|
|
WorkflowID: spec.WorkflowID,
|
|
StepID: spec.StepID,
|
|
AgentProfileID: spec.AgentProfileID,
|
|
})
|
|
}
|
|
|
|
// taskCreatorAdapter adapts the task service to the office TaskCreator interface.
|
|
type taskCreatorAdapter struct {
|
|
taskSvc *taskservice.Service
|
|
}
|
|
|
|
func (a *taskCreatorAdapter) CreateOfficeTask(ctx context.Context, workspaceID, projectID, assigneeAgentID, title, description string) (string, error) {
|
|
task, err := a.taskSvc.CreateTask(ctx, &taskservice.CreateTaskRequest{ //nolint:exhaustruct
|
|
WorkspaceID: workspaceID,
|
|
Title: title,
|
|
Description: description,
|
|
ProjectID: projectID,
|
|
AssigneeAgentProfileID: assigneeAgentID,
|
|
Origin: models.TaskOriginOnboarding,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return task.ID, nil
|
|
}
|
|
|
|
// CreateOfficeTaskInWorkflow creates an office task pinned to a specific
|
|
// workflow id, bypassing the workspace's default office_workflow_id. Used
|
|
// for the standing coordination task that lives on the dedicated
|
|
// Coordination workflow.
|
|
func (a *taskCreatorAdapter) CreateOfficeTaskInWorkflow(
|
|
ctx context.Context, workspaceID, projectID, assigneeAgentID, workflowID, title, description string,
|
|
) (string, error) {
|
|
task, err := a.taskSvc.CreateTask(ctx, &taskservice.CreateTaskRequest{ //nolint:exhaustruct
|
|
WorkspaceID: workspaceID,
|
|
WorkflowID: workflowID,
|
|
Title: title,
|
|
Description: description,
|
|
ProjectID: projectID,
|
|
AssigneeAgentProfileID: assigneeAgentID,
|
|
Origin: models.TaskOriginOnboarding,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return task.ID, nil
|
|
}
|
|
|
|
func (a *taskCreatorAdapter) CreateOfficeSubtask(
|
|
ctx context.Context,
|
|
parentTaskID, assigneeAgentID, title, description string,
|
|
) (string, error) {
|
|
parent, err := a.taskSvc.GetTask(ctx, parentTaskID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return a.taskSvc.CreateChildTask(ctx, parent, taskservice.ChildTaskSpec{
|
|
Title: title,
|
|
Description: description,
|
|
AgentProfileID: assigneeAgentID,
|
|
})
|
|
}
|
|
|
|
// routineWakeupAdapter bridges the routines package's WakeupEnqueuer
|
|
// interface to the concrete office sqlite repo + wakeup dispatcher.
|
|
// Defined here (not in the routines package) so the routines package
|
|
// stays free of office/repository/sqlite imports — keeping the
|
|
// dependency direction routines → wakeup (via interface) and never the
|
|
// reverse.
|
|
type routineWakeupAdapter struct {
|
|
repo *officesqlite.Repository
|
|
dispatcher *officewakeup.Dispatcher
|
|
}
|
|
|
|
func (a *routineWakeupAdapter) CreateWakeupRequest(
|
|
ctx context.Context, req *officeroutines.WakeupRequest,
|
|
) error {
|
|
row := &officesqlite.WakeupRequest{
|
|
ID: req.ID,
|
|
AgentProfileID: req.AgentProfileID,
|
|
Source: req.Source,
|
|
Reason: req.Reason,
|
|
Payload: req.Payload,
|
|
RequestedAt: req.RequestedAt,
|
|
}
|
|
if req.IdempotencyKey != "" {
|
|
row.IdempotencyKey = sql.NullString{String: req.IdempotencyKey, Valid: true}
|
|
}
|
|
return a.repo.CreateWakeupRequest(ctx, row)
|
|
}
|
|
|
|
func (a *routineWakeupAdapter) Dispatch(ctx context.Context, requestID string) error {
|
|
return a.dispatcher.Dispatch(ctx, requestID)
|
|
}
|
|
|
|
// configSyncerAdapter bridges config.ConfigService to the onboarding.ConfigSyncer
|
|
// interface by converting config.ImportResult to onboarding.ApplyResult.
|
|
type configSyncerAdapter struct {
|
|
svc *officeconfig.ConfigService
|
|
}
|
|
|
|
func (a *configSyncerAdapter) ApplyIncoming(ctx context.Context, workspaceID string) (*officeonboarding.ApplyResult, error) {
|
|
result, err := a.svc.ApplyIncoming(ctx, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &officeonboarding.ApplyResult{
|
|
CreatedCount: result.CreatedCount,
|
|
UpdatedCount: result.UpdatedCount,
|
|
}, nil
|
|
}
|
|
|
|
// workflowEnsurerAdapter wraps the task repo to satisfy onboarding.WorkflowEnsurer.
|
|
type workflowEnsurerAdapter struct {
|
|
repo *tasksqlite.Repository
|
|
}
|
|
|
|
func (a *workflowEnsurerAdapter) EnsureOfficeWorkflow(ctx context.Context, workspaceID string) (string, error) {
|
|
return a.repo.EnsureOfficeWorkflow(ctx, workspaceID)
|
|
}
|
|
|
|
func (a *workflowEnsurerAdapter) EnsureOfficeDefaultWorkflow(ctx context.Context, workspaceID string) (string, error) {
|
|
return a.repo.EnsureOfficeDefaultWorkflow(ctx, workspaceID)
|
|
}
|
|
|
|
func (a *workflowEnsurerAdapter) EnsureRoutineWorkflow(ctx context.Context, workspaceID string) (string, error) {
|
|
return a.repo.EnsureRoutineWorkflow(ctx, workspaceID)
|
|
}
|
|
|
|
// workspaceSettingsProviderAdapter implements dashboard.SettingsProvider using
|
|
// the filesystem-based config loader and writer.
|
|
type workspaceSettingsProviderAdapter struct {
|
|
loader *configloader.ConfigLoader
|
|
writer *configloader.FileWriter
|
|
}
|
|
|
|
func (a *workspaceSettingsProviderAdapter) GetSettings(workspaceName string) (*officedashboard.WorkspaceSettings, error) {
|
|
ws, err := a.loader.GetWorkspace(workspaceName)
|
|
if err != nil {
|
|
// Workspace not found in config loader — return defaults.
|
|
return &officedashboard.WorkspaceSettings{
|
|
Name: workspaceName,
|
|
PermissionHandlingMode: "human",
|
|
RecoveryLookbackHours: 24,
|
|
}, nil
|
|
}
|
|
mode := string(ws.Settings.PermissionHandlingMode)
|
|
if mode == "" {
|
|
mode = "human"
|
|
}
|
|
lookback := ws.Settings.RecoveryLookbackHours
|
|
if lookback == 0 {
|
|
lookback = 24
|
|
}
|
|
return &officedashboard.WorkspaceSettings{
|
|
Name: ws.Settings.Name,
|
|
Description: ws.Settings.Description,
|
|
PermissionHandlingMode: mode,
|
|
RecoveryLookbackHours: lookback,
|
|
}, nil
|
|
}
|
|
|
|
func (a *workspaceSettingsProviderAdapter) UpdatePermissionHandlingMode(workspaceName, mode string) error {
|
|
ws, err := a.loader.GetWorkspace(workspaceName)
|
|
if err != nil {
|
|
// Workspace not in loader — write a minimal kandev.yml.
|
|
ws = &configloader.WorkspaceConfig{
|
|
Name: workspaceName,
|
|
Settings: configloader.WorkspaceSettings{
|
|
Name: workspaceName,
|
|
},
|
|
}
|
|
}
|
|
settings := ws.Settings
|
|
settings.PermissionHandlingMode = configloader.PermissionHandlingMode(mode)
|
|
data, err := configloader.MarshalSettings(settings)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal settings: %w", err)
|
|
}
|
|
return a.writer.WriteRawSettings(workspaceName, data)
|
|
}
|
|
|
|
func (a *workspaceSettingsProviderAdapter) UpdateRecoveryLookbackHours(workspaceName string, hours int) error {
|
|
ws, err := a.loader.GetWorkspace(workspaceName)
|
|
if err != nil {
|
|
ws = &configloader.WorkspaceConfig{
|
|
Name: workspaceName,
|
|
Settings: configloader.WorkspaceSettings{
|
|
Name: workspaceName,
|
|
},
|
|
}
|
|
}
|
|
settings := ws.Settings
|
|
settings.RecoveryLookbackHours = hours
|
|
data, err := configloader.MarshalSettings(settings)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal settings: %w", err)
|
|
}
|
|
return a.writer.WriteRawSettings(workspaceName, data)
|
|
}
|