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
11 changes: 9 additions & 2 deletions internal/mcp/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package mcp
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 26 additions & 7 deletions internal/memory/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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 := `
Expand Down
63 changes: 47 additions & 16 deletions internal/memory/task_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"strings"
"time"

Expand Down Expand Up @@ -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(`
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down
9 changes: 7 additions & 2 deletions internal/policy/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down
Loading