134 lines
4.5 KiB
Go
134 lines
4.5 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/linear"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// LinearWatcherSource adapts the Linear integration onto the WatcherSource
|
|
// pipeline. Wraps the existing LinearService (already used by the orchestrator
|
|
// for dedup) and the package-local interpolateLinearPrompt so behaviour is
|
|
// preserved byte-for-byte versus the old createLinearIssueTask helper.
|
|
type LinearWatcherSource struct {
|
|
service LinearService
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewLinearWatcherSource constructs a source bound to the orchestrator's
|
|
// Linear service handle. logger may be nil — methods that log will no-op.
|
|
func NewLinearWatcherSource(svc LinearService, log *logger.Logger) *LinearWatcherSource {
|
|
return &LinearWatcherSource{service: svc, logger: log}
|
|
}
|
|
|
|
func (s *LinearWatcherSource) Name() string { return "linear" }
|
|
|
|
func (s *LinearWatcherSource) Reserve(ctx context.Context, evt any) (bool, error) {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil || e.Issue == nil {
|
|
return false, errors.New("linear source: event payload missing or wrong type")
|
|
}
|
|
// Match today's reserveLinearIssue: nil service is "fail open".
|
|
if s.service == nil {
|
|
return true, nil
|
|
}
|
|
return s.service.ReserveIssueWatchTask(ctx, e.IssueWatchID, e.Issue.Identifier, e.Issue.URL)
|
|
}
|
|
|
|
func (s *LinearWatcherSource) Release(ctx context.Context, evt any) {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil || e.Issue == nil || s.service == nil {
|
|
return
|
|
}
|
|
if err := s.service.ReleaseIssueWatchTask(ctx, e.IssueWatchID, e.Issue.Identifier); err != nil && s.logger != nil {
|
|
s.logger.Warn("linear source: release failed",
|
|
zap.String("identifier", e.Issue.Identifier), zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func (s *LinearWatcherSource) BuildTaskRequest(evt any) (*IssueTaskRequest, error) {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil || e.Issue == nil {
|
|
return nil, errors.New("linear source: event payload missing or wrong type")
|
|
}
|
|
return &IssueTaskRequest{
|
|
WorkspaceID: e.WorkspaceID,
|
|
WorkflowID: e.WorkflowID,
|
|
WorkflowStepID: e.WorkflowStepID,
|
|
Title: fmt.Sprintf("[%s] %s", e.Issue.Identifier, e.Issue.Title),
|
|
Description: interpolateLinearPrompt(e.Prompt, e.Issue),
|
|
Metadata: map[string]interface{}{
|
|
"linear_issue_watch_id": e.IssueWatchID,
|
|
"linear_issue_identifier": e.Issue.Identifier,
|
|
"linear_issue_url": e.Issue.URL,
|
|
"linear_state": e.Issue.StateName,
|
|
"linear_assignee": e.Issue.AssigneeName,
|
|
models.MetaKeyAgentProfileID: e.AgentProfileID,
|
|
models.MetaKeyExecutorProfileID: e.ExecutorProfileID,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *LinearWatcherSource) AttachTaskID(ctx context.Context, evt any, taskID string) error {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil || e.Issue == nil || s.service == nil {
|
|
return nil
|
|
}
|
|
return s.service.AssignIssueWatchTaskID(ctx, e.IssueWatchID, e.Issue.Identifier, taskID)
|
|
}
|
|
|
|
func (s *LinearWatcherSource) WatchID(evt any) string {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil {
|
|
return ""
|
|
}
|
|
return e.IssueWatchID
|
|
}
|
|
|
|
func (s *LinearWatcherSource) MaxInflightTasks(evt any) *int {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil {
|
|
return nil
|
|
}
|
|
return e.MaxInflightTasks
|
|
}
|
|
|
|
func (s *LinearWatcherSource) AutoStartParams(evt any) AutoStartParams {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil {
|
|
return AutoStartParams{}
|
|
}
|
|
return AutoStartParams{
|
|
AgentProfileID: e.AgentProfileID,
|
|
ExecutorProfileID: e.ExecutorProfileID,
|
|
WorkflowStepID: e.WorkflowStepID,
|
|
}
|
|
}
|
|
|
|
// AgentProfileID returns the watcher's bound profile id, or "" when the
|
|
// event payload is malformed. Pre-flight uses "" as the skip-check signal.
|
|
func (s *LinearWatcherSource) AgentProfileID(evt any) string {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil {
|
|
return ""
|
|
}
|
|
return e.AgentProfileID
|
|
}
|
|
|
|
// SelfHeal disables the linear_issue_watches row that produced this event.
|
|
// Nil-safe: with no LinearService wired the call is silently dropped — same
|
|
// pattern as Reserve / Release.
|
|
func (s *LinearWatcherSource) SelfHeal(ctx context.Context, evt any, cause string) error {
|
|
e, ok := evt.(*linear.NewLinearIssueEvent)
|
|
if !ok || e == nil || s.service == nil {
|
|
return nil
|
|
}
|
|
return s.service.DisableIssueWatchWithError(ctx, e.IssueWatchID, cause)
|
|
}
|