diff --git a/internal/mcp/handlers.go b/internal/mcp/handlers.go index 22152a9..705dff8 100644 --- a/internal/mcp/handlers.go +++ b/internal/mcp/handlers.go @@ -4,6 +4,7 @@ package mcp import ( "context" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -153,7 +154,10 @@ func handleCodeExplain(ctx context.Context, repo *memory.Repository, params Code } // Get base path for source code fetching - basePath, _ := config.GetProjectRoot() + basePath, err := config.GetProjectRoot() + if err != nil { + slog.Debug("GetProjectRoot failed in explain handler", "error", err) + } appCtx := app.NewContextForRole(repo, llm.RoleQuery) appCtx.BasePath = basePath @@ -267,7 +271,10 @@ func handleCodeSimplify(ctx context.Context, repo *memory.Repository, params Cod // If file_path provided, read the file content if filePath != "" && code == "" { - projectRoot, _ := config.GetProjectRoot() + projectRoot, err := config.GetProjectRoot() + if err != nil { + slog.Debug("GetProjectRoot failed in simplify handler", "error", err) + } // Validate path to prevent traversal attacks resolvedPath, err := validateAndResolvePath(filePath, projectRoot) diff --git a/internal/memory/sqlite.go b/internal/memory/sqlite.go index 7bed8db..f55e01c 100644 --- a/internal/memory/sqlite.go +++ b/internal/memory/sqlite.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -57,17 +58,35 @@ func NewSQLiteStore(basePath string) (*SQLiteStore, error) { return nil, fmt.Errorf("init schema: %w", err) } - // Migrations - ignore errors for columns that already exist - _, _ = db.Exec(`ALTER TABLE tasks ADD COLUMN complexity TEXT DEFAULT 'medium'`) - - // Interactive planning migrations (phases support) - _, _ = db.Exec(`ALTER TABLE plans ADD COLUMN draft_state TEXT`) - _, _ = db.Exec(`ALTER TABLE plans ADD COLUMN generation_mode TEXT DEFAULT 'batch'`) - _, _ = db.Exec(`ALTER TABLE tasks ADD COLUMN phase_id TEXT REFERENCES phases(id) ON DELETE SET NULL`) + // Migrations — add columns only if they don't already exist + migrateAddColumn(db, "tasks", "complexity", `ALTER TABLE tasks ADD COLUMN complexity TEXT DEFAULT 'medium'`) + migrateAddColumn(db, "plans", "draft_state", `ALTER TABLE plans ADD COLUMN draft_state TEXT`) + migrateAddColumn(db, "plans", "generation_mode", `ALTER TABLE plans ADD COLUMN generation_mode TEXT DEFAULT 'batch'`) + migrateAddColumn(db, "tasks", "phase_id", `ALTER TABLE tasks ADD COLUMN phase_id TEXT REFERENCES phases(id) ON DELETE SET NULL`) return store, nil } +// migrateAddColumn adds a column if it doesn't already exist. +// Logs real errors instead of silently swallowing them. +func migrateAddColumn(db *sql.DB, table, column, ddl string) { + var exists int + err := db.QueryRow( + `SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?`, + table, column, + ).Scan(&exists) + if err != nil { + slog.Warn("migration check failed", "table", table, "column", column, "error", err) + return + } + if exists > 0 { + return // already migrated + } + if _, err := db.Exec(ddl); err != nil { + slog.Warn("migration failed", "table", table, "column", column, "error", err) + } +} + // initSchema creates the database tables if they don't exist. func (s *SQLiteStore) initSchema() error { schema := ` diff --git a/internal/memory/task_store.go b/internal/memory/task_store.go index 9d87775..d7a6936 100644 --- a/internal/memory/task_store.go +++ b/internal/memory/task_store.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "log/slog" "strings" "time" @@ -156,9 +157,11 @@ func (s *SQLiteStore) CreatePlan(p *task.Plan) error { // Serialize draft state if present var draftStateJSON interface{} if p.DraftState != nil { - if data, err := json.Marshal(p.DraftState); err == nil { - draftStateJSON = string(data) + data, err := json.Marshal(p.DraftState) + if err != nil { + return fmt.Errorf("marshal draft_state: %w", err) } + draftStateJSON = string(data) } if _, err = tx.Exec(` @@ -207,7 +210,9 @@ func (s *SQLiteStore) GetPlan(id string) (*task.Plan, error) { } if draftStateJSON.Valid && draftStateJSON.String != "" { var draftState task.PlanDraftState - if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err == nil { + if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err != nil { + slog.Warn("corrupt draft_state JSON", "plan", p.ID, "error", err) + } else { p.DraftState = &draftState } } @@ -257,7 +262,9 @@ func (s *SQLiteStore) ListPlans() ([]task.Plan, error) { } if draftStateJSON.Valid && draftStateJSON.String != "" { var draftState task.PlanDraftState - if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err == nil { + if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err != nil { + slog.Warn("corrupt draft_state JSON", "plan", p.ID, "error", err) + } else { p.DraftState = &draftState } } @@ -459,7 +466,9 @@ func (s *SQLiteStore) GetClarifySession(id string) (*task.ClarifySession, error) session.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) session.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) if currentQuestionsJSON.Valid && currentQuestionsJSON.String != "" { - _ = json.Unmarshal([]byte(currentQuestionsJSON.String), &session.CurrentQuestions) + if err := json.Unmarshal([]byte(currentQuestionsJSON.String), &session.CurrentQuestions); err != nil { + slog.Warn("corrupt current_questions JSON", "session", session.ID, "error", err) + } } return &session, nil @@ -571,10 +580,14 @@ func (s *SQLiteStore) ListClarifyTurns(sessionID string) ([]task.ClarifyTurn, er turn.MaxRoundsReached = maxRoundsReachedInt == 1 turn.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) if questionsJSON.Valid && questionsJSON.String != "" { - _ = json.Unmarshal([]byte(questionsJSON.String), &turn.Questions) + if err := json.Unmarshal([]byte(questionsJSON.String), &turn.Questions); err != nil { + slog.Warn("corrupt questions JSON", "turn", turn.ID, "error", err) + } } if answersJSON.Valid && answersJSON.String != "" { - _ = json.Unmarshal([]byte(answersJSON.String), &turn.Answers) + if err := json.Unmarshal([]byte(answersJSON.String), &turn.Answers); err != nil { + slog.Warn("corrupt answers JSON", "turn", turn.ID, "error", err) + } } turns = append(turns, turn) } @@ -648,25 +661,39 @@ func scanTaskRow(row taskRowScanner) (task.Task, error) { } if acJSON.Valid && acJSON.String != "" { - _ = json.Unmarshal([]byte(acJSON.String), &t.AcceptanceCriteria) + if err := json.Unmarshal([]byte(acJSON.String), &t.AcceptanceCriteria); err != nil { + slog.Warn("corrupt acceptance_criteria JSON", "task", t.ID, "error", err) + } } if vsJSON.Valid && vsJSON.String != "" { - _ = json.Unmarshal([]byte(vsJSON.String), &t.ValidationSteps) + if err := json.Unmarshal([]byte(vsJSON.String), &t.ValidationSteps); err != nil { + slog.Warn("corrupt validation_steps JSON", "task", t.ID, "error", err) + } } if keywordsJSON.Valid && keywordsJSON.String != "" { - _ = json.Unmarshal([]byte(keywordsJSON.String), &t.Keywords) + if err := json.Unmarshal([]byte(keywordsJSON.String), &t.Keywords); err != nil { + slog.Warn("corrupt keywords JSON", "task", t.ID, "error", err) + } } if queriesJSON.Valid && queriesJSON.String != "" { - _ = json.Unmarshal([]byte(queriesJSON.String), &t.SuggestedAskQueries) + if err := json.Unmarshal([]byte(queriesJSON.String), &t.SuggestedAskQueries); err != nil { + slog.Warn("corrupt suggested_ask_queries JSON", "task", t.ID, "error", err) + } } if filesJSON.Valid && filesJSON.String != "" { - _ = json.Unmarshal([]byte(filesJSON.String), &t.FilesModified) + if err := json.Unmarshal([]byte(filesJSON.String), &t.FilesModified); err != nil { + slog.Warn("corrupt files_modified JSON", "task", t.ID, "error", err) + } } if expectedFilesJSON.Valid && expectedFilesJSON.String != "" { - _ = json.Unmarshal([]byte(expectedFilesJSON.String), &t.ExpectedFiles) + if err := json.Unmarshal([]byte(expectedFilesJSON.String), &t.ExpectedFiles); err != nil { + slog.Warn("corrupt expected_files JSON", "task", t.ID, "error", err) + } } if gitBaselineJSON.Valid && gitBaselineJSON.String != "" { - _ = json.Unmarshal([]byte(gitBaselineJSON.String), &t.GitBaseline) + if err := json.Unmarshal([]byte(gitBaselineJSON.String), &t.GitBaseline); err != nil { + slog.Warn("corrupt git_baseline JSON", "task", t.ID, "error", err) + } } return t, nil @@ -1144,7 +1171,9 @@ func (s *SQLiteStore) SearchPlans(query string, status task.PlanStatus) ([]task. } if draftStateJSON.Valid && draftStateJSON.String != "" { var draftState task.PlanDraftState - if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err == nil { + if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err != nil { + slog.Warn("corrupt draft_state JSON", "plan", p.ID, "error", err) + } else { p.DraftState = &draftState } } @@ -1190,7 +1219,9 @@ func (s *SQLiteStore) GetActivePlan() (*task.Plan, error) { } if draftStateJSON.Valid && draftStateJSON.String != "" { var draftState task.PlanDraftState - if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err == nil { + if err := json.Unmarshal([]byte(draftStateJSON.String), &draftState); err != nil { + slog.Warn("corrupt draft_state JSON", "plan", p.ID, "error", err) + } else { p.DraftState = &draftState } } diff --git a/internal/policy/audit.go b/internal/policy/audit.go index fbbdd7d..7f312f0 100644 --- a/internal/policy/audit.go +++ b/internal/policy/audit.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "fmt" + "log/slog" "time" "github.com/google/uuid" @@ -202,7 +203,9 @@ func (s *AuditStore) scanDecision(row *sql.Row) (*PolicyDecision, error) { d.Violations = ParseViolations(violationsJSON) if inputJSON != "" && inputJSON != "{}" { var input any - if err := json.Unmarshal([]byte(inputJSON), &input); err == nil { + if err := json.Unmarshal([]byte(inputJSON), &input); err != nil { + slog.Warn("corrupt policy decision input JSON", "id", d.DecisionID, "error", err) + } else { d.Input = input } } @@ -238,7 +241,9 @@ func (s *AuditStore) scanDecisionRows(rows *sql.Rows) (*PolicyDecision, error) { d.Violations = ParseViolations(violationsJSON) if inputJSON != "" && inputJSON != "{}" { var input any - if err := json.Unmarshal([]byte(inputJSON), &input); err == nil { + if err := json.Unmarshal([]byte(inputJSON), &input); err != nil { + slog.Warn("corrupt policy decision input JSON", "id", d.DecisionID, "error", err) + } else { d.Input = input } }