Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: latest
version: "v2.11.3"

test:
name: Test
Expand Down
50 changes: 26 additions & 24 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
version: "2"

run:
timeout: 5m

formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/entire-vc/evc-mesh

linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
- gocritic

linters-settings:
govet:
shadow: true
goimports:
local-prefixes: github.com/entire-vc/evc-mesh
gocritic:
enabled-tags:
- diagnostic
- style

issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- unparam
- path: mock_
linters:
- unused
- unparam
settings:
gocritic:
enabled-tags:
- diagnostic
- style
exclusions:
rules:
- path: _test\.go
linters:
- errcheck
- unparam
- path: mock_
linters:
- unused
- unparam
3 changes: 2 additions & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ func main() {
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
log.Println("Connected to PostgreSQL")

// 3. Run database migrations.
if err := goose.Up(db.DB, "migrations"); err != nil {
_ = db.Close()
log.Fatalf("Failed to run migrations: %v", err)
}
log.Println("Database migrations applied")
defer func() { _ = db.Close() }()

// 4. Create all repository instances.
workspaceRepo := postgres.NewWorkspaceRepo(db)
Expand Down
24 changes: 12 additions & 12 deletions internal/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ var timeNow = time.Now

// Errors returned by the auth service.
var (
ErrPasswordTooShort = apierror.BadRequest("password must be at least 8 characters")
ErrPasswordTooLong = apierror.BadRequest("password must be at most 128 characters")
ErrPasswordTooShort = apierror.BadRequest("password must be at least 8 characters")
ErrPasswordTooLong = apierror.BadRequest("password must be at most 128 characters")
ErrPasswordWeakComplexity = apierror.BadRequest("password must contain at least one uppercase letter, one lowercase letter, and one digit")
ErrInvalidEmail = apierror.BadRequest("invalid email address")
ErrEmailAlreadyExists = apierror.Conflict("a user with this email already exists")
ErrInvalidCredentials = apierror.Unauthorized("invalid email or password")
ErrInvalidRefreshToken = apierror.Unauthorized("invalid refresh token")
ErrRefreshTokenExpired = apierror.Unauthorized("refresh token has expired")
ErrRefreshTokenRevoked = apierror.Unauthorized("refresh token has been revoked")
ErrTokenReused = apierror.Unauthorized("refresh token reuse detected; all sessions revoked")
ErrInvalidAccessToken = apierror.Unauthorized("invalid or expired access token")
ErrUserInactive = apierror.Unauthorized("user account is inactive")
ErrInvalidEmail = apierror.BadRequest("invalid email address")
ErrEmailAlreadyExists = apierror.Conflict("a user with this email already exists")
ErrInvalidCredentials = apierror.Unauthorized("invalid email or password")
ErrInvalidRefreshToken = apierror.Unauthorized("invalid refresh token")
ErrRefreshTokenExpired = apierror.Unauthorized("refresh token has expired")
ErrRefreshTokenRevoked = apierror.Unauthorized("refresh token has been revoked")
ErrTokenReused = apierror.Unauthorized("refresh token reuse detected; all sessions revoked")
ErrInvalidAccessToken = apierror.Unauthorized("invalid or expired access token")
ErrUserInactive = apierror.Unauthorized("user account is inactive")
)

// Claims represents the JWT claims for an access token.
Expand Down Expand Up @@ -308,7 +308,7 @@ func (s *Service) generateTokenPair(user *domain.User) (*TokenPair, error) {

// generateRefreshToken creates a random refresh token and its SHA-256 hash.
// Format: rt_{64_hex_chars}
func generateRefreshToken() (plainToken string, tokenHash string, err error) {
func generateRefreshToken() (plainToken, tokenHash string, err error) {
b := make([]byte, refreshTokenRandomBytes)
if _, err := rand.Read(b); err != nil {
return "", "", err
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ func TestValidateAccessToken_Malformed(t *testing.T) {
func TestValidatePassword(t *testing.T) {
assert.NoError(t, ValidatePassword("StrongP4ss"))
assert.NoError(t, ValidatePassword("A1bcdefg"))
assert.Error(t, ValidatePassword("short1A")) // too short
assert.Error(t, ValidatePassword("short1A")) // too short
assert.Error(t, ValidatePassword("alllower1")) // no uppercase
assert.Error(t, ValidatePassword("ALLUPPER1")) // no lowercase
assert.Error(t, ValidatePassword("NoDigits")) // no digit
Expand Down
18 changes: 9 additions & 9 deletions internal/domain/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ type Agent struct {
// Profile fields (Sprint 20 — team directory & assignment rules)
// Note: AgentType above is the automation category (claude_code/openclaw/etc).
// Role is the team role (developer/lead/reviewer/etc).
Role string `json:"role" db:"role"`
ResponsibilityZone string `json:"responsibility_zone" db:"responsibility_zone"`
Role string `json:"role" db:"role"`
ResponsibilityZone string `json:"responsibility_zone" db:"responsibility_zone"`
EscalationTo *json.RawMessage `json:"escalation_to,omitempty" db:"escalation_to"`
AcceptsFrom json.RawMessage `json:"accepts_from" db:"accepts_from"`
MaxConcurrentTasks int `json:"max_concurrent_tasks" db:"max_concurrent_tasks"`
WorkingHours string `json:"working_hours" db:"working_hours"`
ProfileDescription string `json:"profile_description" db:"profile_description"`
CallbackURL string `json:"callback_url" db:"callback_url"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
AcceptsFrom json.RawMessage `json:"accepts_from" db:"accepts_from"`
MaxConcurrentTasks int `json:"max_concurrent_tasks" db:"max_concurrent_tasks"`
WorkingHours string `json:"working_hours" db:"working_hours"`
ProfileDescription string `json:"profile_description" db:"profile_description"`
CallbackURL string `json:"callback_url" db:"callback_url"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}

// DefaultHeartbeatStaleThreshold is the default time after which an agent's heartbeat is considered stale.
Expand Down
34 changes: 17 additions & 17 deletions internal/domain/agent_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,21 @@ func (c ComplianceDetail) ComputeScore() float32 {
// Sessions accumulate metrics (tool calls, tokens, cost, compliance) for monitoring
// and are used to detect stale/abandoned sessions via EndStale.
type AgentSession struct {
ID uuid.UUID `json:"id" db:"id"`
WorkspaceID uuid.UUID `json:"workspace_id" db:"workspace_id"`
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
StartedAt time.Time `json:"started_at" db:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty" db:"ended_at"`
Status AgentSessionStatus `json:"status" db:"status"`
ToolCalls int `json:"tool_calls" db:"tool_calls"`
ToolBreakdown json.RawMessage `json:"tool_breakdown" db:"tool_breakdown"`
TasksTouched []uuid.UUID `json:"tasks_touched" db:"tasks_touched"`
EventsPublished int `json:"events_published" db:"events_published"`
MemoriesCreated int `json:"memories_created" db:"memories_created"`
ModelUsed string `json:"model_used" db:"model_used"`
TokensIn int64 `json:"tokens_in" db:"tokens_in"`
TokensOut int64 `json:"tokens_out" db:"tokens_out"`
EstimatedCost float64 `json:"estimated_cost" db:"estimated_cost"`
ComplianceScore float32 `json:"compliance_score" db:"compliance_score"`
ComplianceDetail json.RawMessage `json:"compliance_detail" db:"compliance_detail"`
ID uuid.UUID `json:"id" db:"id"`
WorkspaceID uuid.UUID `json:"workspace_id" db:"workspace_id"`
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
StartedAt time.Time `json:"started_at" db:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty" db:"ended_at"`
Status AgentSessionStatus `json:"status" db:"status"`
ToolCalls int `json:"tool_calls" db:"tool_calls"`
ToolBreakdown json.RawMessage `json:"tool_breakdown" db:"tool_breakdown"`
TasksTouched []uuid.UUID `json:"tasks_touched" db:"tasks_touched"`
EventsPublished int `json:"events_published" db:"events_published"`
MemoriesCreated int `json:"memories_created" db:"memories_created"`
ModelUsed string `json:"model_used" db:"model_used"`
TokensIn int64 `json:"tokens_in" db:"tokens_in"`
TokensOut int64 `json:"tokens_out" db:"tokens_out"`
EstimatedCost float64 `json:"estimated_cost" db:"estimated_cost"`
ComplianceScore float32 `json:"compliance_score" db:"compliance_score"`
ComplianceDetail json.RawMessage `json:"compliance_detail" db:"compliance_detail"`
}
34 changes: 17 additions & 17 deletions internal/domain/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ import (

// NotificationPreference stores per-actor notification subscription settings.
type NotificationPreference struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
UserID *uuid.UUID `db:"user_id" json:"user_id"`
AgentID *uuid.UUID `db:"agent_id" json:"agent_id"`
Channel string `db:"channel" json:"channel"`
Events pq.StringArray `db:"events" json:"events"`
IsEnabled bool `db:"is_enabled" json:"is_enabled"`
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
UserID *uuid.UUID `db:"user_id" json:"user_id"`
AgentID *uuid.UUID `db:"agent_id" json:"agent_id"`
Channel string `db:"channel" json:"channel"`
Events pq.StringArray `db:"events" json:"events"`
IsEnabled bool `db:"is_enabled" json:"is_enabled"`
Config json.RawMessage `db:"config" json:"config"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

// Notification is a persisted in-app notification for a user.
type Notification struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
UserID *uuid.UUID `db:"user_id" json:"user_id"`
EventType string `db:"event_type" json:"event_type"`
Title string `db:"title" json:"title"`
Body string `db:"body" json:"body"`
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
UserID *uuid.UUID `db:"user_id" json:"user_id"`
EventType string `db:"event_type" json:"event_type"`
Title string `db:"title" json:"title"`
Body string `db:"body" json:"body"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
IsRead bool `db:"is_read" json:"is_read"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
IsRead bool `db:"is_read" json:"is_read"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}

// NotificationEvent carries the data used to build and dispatch a notification.
Expand Down
4 changes: 2 additions & 2 deletions internal/domain/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ func (m *ProjectMember) IsAgentMember() bool {
// ProjectMemberWithUser embeds ProjectMember with the associated user's brief info.
type ProjectMemberWithUser struct {
ProjectMember
User *UserBrief `json:"user,omitempty"`
AgentName string `json:"agent_name,omitempty"`
User *UserBrief `json:"user,omitempty"`
AgentName string `json:"agent_name,omitempty"`
}

// Project belongs to a Workspace and contains tasks, statuses, and custom fields.
Expand Down
4 changes: 2 additions & 2 deletions internal/domain/project_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ type TextItem struct {

// ProjectUpdateMetrics holds auto-populated task count metrics for an update.
type ProjectUpdateMetrics struct {
TasksCompleted int `json:"tasks_completed"`
TasksTotal int `json:"tasks_total"`
TasksCompleted int `json:"tasks_completed"`
TasksTotal int `json:"tasks_total"`
TasksInProgress int `json:"tasks_in_progress"`
}

Expand Down
54 changes: 27 additions & 27 deletions internal/domain/recurring.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,37 @@ const (
// RecurringSchedule defines a template and schedule for automatically creating task instances.
// Each instance is a full task with its own comments, artifacts, and activity log.
type RecurringSchedule struct {
ID uuid.UUID `json:"id" db:"id"`
WorkspaceID uuid.UUID `json:"workspace_id" db:"workspace_id"`
ProjectID uuid.UUID `json:"project_id" db:"project_id"`
ID uuid.UUID `json:"id" db:"id"`
WorkspaceID uuid.UUID `json:"workspace_id" db:"workspace_id"`
ProjectID uuid.UUID `json:"project_id" db:"project_id"`

TitleTemplate string `json:"title_template" db:"title_template"`
DescriptionTemplate string `json:"description_template" db:"description_template"`
TitleTemplate string `json:"title_template" db:"title_template"`
DescriptionTemplate string `json:"description_template" db:"description_template"`

Frequency RecurringFrequency `json:"frequency" db:"frequency"`
CronExpr string `json:"cron_expr" db:"cron_expr"`
Timezone string `json:"timezone" db:"timezone"`
Frequency RecurringFrequency `json:"frequency" db:"frequency"`
CronExpr string `json:"cron_expr" db:"cron_expr"`
Timezone string `json:"timezone" db:"timezone"`

AssigneeID *uuid.UUID `json:"assignee_id" db:"assignee_id"`
AssigneeType AssigneeType `json:"assignee_type" db:"assignee_type"`
Priority Priority `json:"priority" db:"priority"`
Labels pq.StringArray `json:"labels" db:"labels"`
StatusID *uuid.UUID `json:"status_id" db:"status_id"`
AssigneeID *uuid.UUID `json:"assignee_id" db:"assignee_id"`
AssigneeType AssigneeType `json:"assignee_type" db:"assignee_type"`
Priority Priority `json:"priority" db:"priority"`
Labels pq.StringArray `json:"labels" db:"labels"`
StatusID *uuid.UUID `json:"status_id" db:"status_id"`

IsActive bool `json:"is_active" db:"is_active"`
StartsAt time.Time `json:"starts_at" db:"starts_at"`
EndsAt *time.Time `json:"ends_at" db:"ends_at"`
MaxInstances *int `json:"max_instances" db:"max_instances"`
IsActive bool `json:"is_active" db:"is_active"`
StartsAt time.Time `json:"starts_at" db:"starts_at"`
EndsAt *time.Time `json:"ends_at" db:"ends_at"`
MaxInstances *int `json:"max_instances" db:"max_instances"`

NextRunAt *time.Time `json:"next_run_at" db:"next_run_at"`
LastTriggeredAt *time.Time `json:"last_triggered_at" db:"last_triggered_at"`
InstanceCount int `json:"instance_count" db:"instance_count"`
NextRunAt *time.Time `json:"next_run_at" db:"next_run_at"`
LastTriggeredAt *time.Time `json:"last_triggered_at" db:"last_triggered_at"`
InstanceCount int `json:"instance_count" db:"instance_count"`

CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
CreatedByType ActorType `json:"created_by_type" db:"created_by_type"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty" db:"deleted_at"`
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
CreatedByType ActorType `json:"created_by_type" db:"created_by_type"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty" db:"deleted_at"`
}

// RecurringInstanceSummary is a lightweight view of one task instance in a series.
Expand All @@ -63,7 +63,7 @@ type RecurringInstanceSummary struct {
CompletedAt *time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
// LastComment is the most recent comment body (truncated to 2000 chars), nil if no comments.
LastComment *string `json:"last_comment,omitempty"`
LastComment *string `json:"last_comment,omitempty"`
// ArtifactCount is the number of artifacts attached to this instance.
ArtifactCount int `json:"artifact_count"`
ArtifactCount int `json:"artifact_count"`
}
Loading
Loading