summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/api/deployment.go4
-rw-r--r--internal/api/elaborate.go12
-rw-r--r--internal/api/projects.go71
-rw-r--r--internal/api/server.go17
-rw-r--r--internal/api/server_test.go72
-rw-r--r--internal/api/task_view.go4
-rw-r--r--internal/api/webhook_test.go8
-rw-r--r--internal/executor/container.go37
-rw-r--r--internal/storage/db.go71
-rw-r--r--internal/storage/db_test.go66
-rw-r--r--internal/storage/seed.go46
-rw-r--r--internal/task/project.go11
-rw-r--r--internal/task/task.go3
-rw-r--r--internal/task/validator_test.go1
14 files changed, 341 insertions, 82 deletions
diff --git a/internal/api/deployment.go b/internal/api/deployment.go
index d927545..8972fe2 100644
--- a/internal/api/deployment.go
+++ b/internal/api/deployment.go
@@ -23,7 +23,7 @@ func (s *Server) handleGetDeploymentStatus(w http.ResponseWriter, r *http.Reques
if err != nil {
if err == sql.ErrNoRows {
// No execution yet — return status with no fix commits.
- status := deployment.Check(nil, tk.Agent.ProjectDir)
+ status := deployment.Check(nil, tk.RepositoryURL)
writeJSON(w, http.StatusOK, status)
return
}
@@ -31,6 +31,6 @@ func (s *Server) handleGetDeploymentStatus(w http.ResponseWriter, r *http.Reques
return
}
- status := deployment.Check(exec.Commits, tk.Agent.ProjectDir)
+ status := deployment.Check(exec.Commits, tk.RepositoryURL)
writeJSON(w, http.StatusOK, status)
}
diff --git a/internal/api/elaborate.go b/internal/api/elaborate.go
index 0c681ae..2c164d3 100644
--- a/internal/api/elaborate.go
+++ b/internal/api/elaborate.go
@@ -281,7 +281,9 @@ func (s *Server) handleElaborateTask(w http.ResponseWriter, r *http.Request) {
}
var input struct {
- Prompt string `json:"prompt"`
+ Prompt string `json:"prompt"`
+ ProjectID string `json:"project_id"`
+ // project_dir kept for backward compat; project_id takes precedence
ProjectDir string `json:"project_dir"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
@@ -294,11 +296,15 @@ func (s *Server) handleElaborateTask(w http.ResponseWriter, r *http.Request) {
}
workDir := s.workDir
- if input.ProjectDir != "" {
+ if input.ProjectID != "" {
+ if proj, err := s.store.GetProject(input.ProjectID); err == nil {
+ workDir = proj.LocalPath
+ }
+ } else if input.ProjectDir != "" {
workDir = input.ProjectDir
}
- if input.ProjectDir != "" {
+ if workDir != s.workDir {
go s.appendRawNarrative(workDir, input.Prompt)
}
diff --git a/internal/api/projects.go b/internal/api/projects.go
new file mode 100644
index 0000000..d3dbbf9
--- /dev/null
+++ b/internal/api/projects.go
@@ -0,0 +1,71 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/google/uuid"
+ "github.com/thepeterstone/claudomator/internal/task"
+)
+
+func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
+ projects, err := s.store.ListProjects()
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ if projects == nil {
+ projects = []*task.Project{}
+ }
+ writeJSON(w, http.StatusOK, projects)
+}
+
+func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
+ var p task.Project
+ if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
+ return
+ }
+ if p.Name == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
+ return
+ }
+ if p.ID == "" {
+ p.ID = uuid.New().String()
+ }
+ if err := s.store.CreateProject(&p); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusCreated, p)
+}
+
+func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ p, err := s.store.GetProject(id)
+ if err != nil {
+ writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
+ return
+ }
+ writeJSON(w, http.StatusOK, p)
+}
+
+func (s *Server) handleUpdateProject(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ existing, err := s.store.GetProject(id)
+ if err != nil {
+ writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
+ return
+ }
+ if err := json.NewDecoder(r.Body).Decode(existing); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
+ return
+ }
+ existing.ID = id // ensure ID cannot be changed via body
+ if err := s.store.UpdateProject(existing); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, existing)
+}
+
diff --git a/internal/api/server.go b/internal/api/server.go
index 0127ab9..65823b4 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -133,6 +133,10 @@ func (s *Server) routes() {
s.mux.HandleFunc("GET /api/ws", s.handleWebSocket)
s.mux.HandleFunc("GET /api/workspaces", s.handleListWorkspaces)
s.mux.HandleFunc("GET /api/tasks/{id}/deployment-status", s.handleGetDeploymentStatus)
+ s.mux.HandleFunc("GET /api/projects", s.handleListProjects)
+ s.mux.HandleFunc("POST /api/projects", s.handleCreateProject)
+ s.mux.HandleFunc("GET /api/projects/{id}", s.handleGetProject)
+ s.mux.HandleFunc("PUT /api/projects/{id}", s.handleUpdateProject)
s.mux.HandleFunc("GET /api/health", s.handleHealth)
s.mux.HandleFunc("POST /api/webhooks/github", s.handleGitHubWebhook)
s.mux.HandleFunc("GET /api/push/vapid-key", s.handleGetVAPIDKey)
@@ -153,16 +157,7 @@ func (s *Server) forwardResults() {
}
// processResult broadcasts a task completion event via WebSocket and calls the notifier if set.
-// It also parses git diff stats from the execution stdout log and persists them.
func (s *Server) processResult(result *executor.Result) {
- if result.Execution.StdoutPath != "" {
- if stats := parseChangestatFromFile(result.Execution.StdoutPath); stats != nil {
- if err := s.store.UpdateExecutionChangestats(result.Execution.ID, stats); err != nil {
- s.logger.Error("failed to store changestats", "execID", result.Execution.ID, "error", err)
- }
- }
- }
-
event := map[string]interface{}{
"type": "task_completed",
"task_id": result.TaskID,
@@ -463,10 +458,6 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
ParentTaskID: input.ParentTaskID,
}
- // Fallback for repository_url if only provided in Agent config
- if t.RepositoryURL == "" && input.Agent.ProjectDir != "" {
- t.RepositoryURL = input.Agent.ProjectDir
- }
if t.Agent.Type == "" {
t.Agent.Type = "claude"
}
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index 8ff4227..27fc645 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -1641,34 +1641,31 @@ func TestRunTask_AgentCancelled_TaskSetToCancelled(t *testing.T) {
}
}
-// TestGetTask_IncludesChangestats verifies that after processResult parses git diff stats
-// from the execution stdout log, they appear in the execution history response.
+// TestGetTask_IncludesChangestats verifies that changestats stored on an execution
+// are returned correctly by GET /api/tasks/{id}/executions.
func TestGetTask_IncludesChangestats(t *testing.T) {
srv, store := testServer(t)
tk := createTaskWithState(t, store, "cs-task-1", task.StateCompleted)
- // Write a stdout log with a git diff --stat summary line.
- dir := t.TempDir()
- stdoutPath := filepath.Join(dir, "stdout.log")
- logContent := "Agent output line 1\n3 files changed, 50 insertions(+), 10 deletions(-)\nAgent output line 2\n"
- if err := os.WriteFile(stdoutPath, []byte(logContent), 0600); err != nil {
- t.Fatal(err)
- }
-
exec := &storage.Execution{
- ID: "cs-exec-1",
- TaskID: tk.ID,
- StartTime: time.Now().UTC(),
- EndTime: time.Now().UTC().Add(time.Minute),
- Status: "COMPLETED",
- StdoutPath: stdoutPath,
+ ID: "cs-exec-1",
+ TaskID: tk.ID,
+ StartTime: time.Now().UTC(),
+ EndTime: time.Now().UTC().Add(time.Minute),
+ Status: "COMPLETED",
}
if err := store.CreateExecution(exec); err != nil {
t.Fatal(err)
}
- // processResult should parse changestats from the stdout log and store them.
+ // Pool stores changestats after execution; simulate by calling UpdateExecutionChangestats directly.
+ cs := &task.Changestats{FilesChanged: 3, LinesAdded: 50, LinesRemoved: 10}
+ if err := store.UpdateExecutionChangestats(exec.ID, cs); err != nil {
+ t.Fatal(err)
+ }
+
+ // processResult broadcasts but does NOT parse changestats (that's the pool's job).
result := &executor.Result{
TaskID: tk.ID,
Execution: exec,
@@ -1976,3 +1973,44 @@ func TestListTasks_NonReadyTask_OmitsDeploymentStatus(t *testing.T) {
t.Error("PENDING task should not include deployment_status field")
}
}
+
+func TestProjects_CRUD(t *testing.T) {
+ srv, _ := testServer(t)
+
+ // Create
+ body := `{"name":"testproj","local_path":"/workspace/testproj","type":"web"}`
+ req := httptest.NewRequest("POST", "/api/projects", strings.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+ if w.Code != http.StatusCreated {
+ t.Fatalf("POST /api/projects: want 201, got %d; body: %s", w.Code, w.Body.String())
+ }
+ var created map[string]interface{}
+ json.NewDecoder(w.Body).Decode(&created)
+ id, _ := created["id"].(string)
+ if id == "" {
+ t.Fatal("created project has no id")
+ }
+
+ // Get
+ req = httptest.NewRequest("GET", "/api/projects/"+id, nil)
+ w = httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+ if w.Code != http.StatusOK {
+ t.Fatalf("GET /api/projects/%s: want 200, got %d", id, w.Code)
+ }
+
+ // List
+ req = httptest.NewRequest("GET", "/api/projects", nil)
+ w = httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+ if w.Code != http.StatusOK {
+ t.Fatalf("GET /api/projects: want 200, got %d", w.Code)
+ }
+ var list []interface{}
+ json.NewDecoder(w.Body).Decode(&list)
+ if len(list) == 0 {
+ t.Error("expected at least one project in list")
+ }
+}
diff --git a/internal/api/task_view.go b/internal/api/task_view.go
index 5791058..e6e7097 100644
--- a/internal/api/task_view.go
+++ b/internal/api/task_view.go
@@ -30,12 +30,12 @@ func (s *Server) enrichTask(tk *task.Task) *taskView {
if err != nil {
if err == sql.ErrNoRows {
// No execution yet — still include deployment status (empty commits).
- view.DeploymentStatus = deployment.Check(nil, tk.Agent.ProjectDir)
+ view.DeploymentStatus = deployment.Check(nil, tk.RepositoryURL)
}
return view
}
view.Changestats = exec.Changestats
- view.DeploymentStatus = deployment.Check(exec.Commits, tk.Agent.ProjectDir)
+ view.DeploymentStatus = deployment.Check(exec.Commits, tk.RepositoryURL)
return view
}
diff --git a/internal/api/webhook_test.go b/internal/api/webhook_test.go
index 0fc9664..967b62b 100644
--- a/internal/api/webhook_test.go
+++ b/internal/api/webhook_test.go
@@ -380,9 +380,9 @@ func TestGitHubWebhook_FallbackToSingleProject(t *testing.T) {
}
}
-func TestGitHubWebhook_NoProjectsConfigured_CreatesTaskWithoutProjectDir(t *testing.T) {
+func TestGitHubWebhook_NoProjectsConfigured_CreatesTaskWithGitHubURL(t *testing.T) {
srv, store := testServer(t)
- // No projects configured — task should still be created, just no project dir set.
+ // No projects configured — task should still be created with the GitHub remote URL.
w := webhookPost(t, srv, "check_run", checkRunFailurePayload, "")
@@ -395,8 +395,8 @@ func TestGitHubWebhook_NoProjectsConfigured_CreatesTaskWithoutProjectDir(t *test
if err != nil {
t.Fatalf("task not found: %v", err)
}
- if tk.Agent.ProjectDir != "" {
- t.Errorf("expected empty project dir, got %q", tk.Agent.ProjectDir)
+ if tk.RepositoryURL == "" {
+ t.Error("expected non-empty repository_url from GitHub webhook payload")
}
}
diff --git a/internal/executor/container.go b/internal/executor/container.go
index ba0c03a..2c5b7d3 100644
--- a/internal/executor/container.go
+++ b/internal/executor/container.go
@@ -48,20 +48,7 @@ func (r *ContainerRunner) Run(ctx context.Context, t *task.Task, e *storage.Exec
var err error
repoURL := t.RepositoryURL
if repoURL == "" {
- repoURL = t.Agent.RepositoryURL
- }
- if repoURL == "" {
- // Fallback to project_dir if repository_url is not set (legacy support).
- // Prefer the 'local' bare remote so that git push succeeds after execution
- // (pushing to a non-bare working copy on a checked-out branch is rejected by git).
- if t.Agent.ProjectDir != "" {
- repoURL = t.Agent.ProjectDir
- if out, err2 := exec.Command("git", "-C", t.Agent.ProjectDir, "remote", "get-url", "local").Output(); err2 == nil {
- repoURL = strings.TrimSpace(string(out))
- }
- } else {
- return fmt.Errorf("task %s has no repository_url or project_dir", t.ID)
- }
+ return fmt.Errorf("task %s has no repository_url", t.ID)
}
image := t.Agent.ContainerImage
@@ -362,25 +349,3 @@ func (r *ContainerRunner) buildInnerCmd(t *task.Task, e *storage.Execution, isRe
return []string{"sh", "-c", claudeCmd.String()}
}
-
-func (r *ContainerRunner) fallbackGitInit(repoURL, workspace string) error {
- // Ensure directory exists
- if err := os.MkdirAll(workspace, 0755); err != nil {
- return err
- }
- // If it's a local directory but not a repo, init it.
- cmds := [][]string{
- gitSafe("-C", workspace, "init"),
- gitSafe("-C", workspace, "add", "-A"),
- gitSafe("-C", workspace, "commit", "--allow-empty", "-m", "chore: initial commit"),
- }
- // If it was a local path, maybe we should have copied it?
- // git clone handle local paths fine if they are repos.
- // This fallback is only if it's NOT a repo.
- for _, args := range cmds {
- if out, err := r.command(context.Background(), "git", args...).CombinedOutput(); err != nil {
- return fmt.Errorf("git init failed: %w\n%s", err, out)
- }
- }
- return nil
-}
diff --git a/internal/storage/db.go b/internal/storage/db.go
index 1a0e74f..8f834b2 100644
--- a/internal/storage/db.go
+++ b/internal/storage/db.go
@@ -109,6 +109,16 @@ func (s *DB) migrate() error {
)`,
`CREATE INDEX IF NOT EXISTS idx_agent_events_agent ON agent_events(agent)`,
`CREATE INDEX IF NOT EXISTS idx_agent_events_timestamp ON agent_events(timestamp)`,
+ `CREATE TABLE IF NOT EXISTS projects (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ remote_url TEXT NOT NULL DEFAULT '',
+ local_path TEXT NOT NULL DEFAULT '',
+ type TEXT NOT NULL DEFAULT 'web',
+ deploy_script TEXT NOT NULL DEFAULT '',
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME NOT NULL
+ )`,
}
for _, m := range migrations {
if _, err := s.db.Exec(m); err != nil {
@@ -1056,3 +1066,64 @@ func timeOrNull(t *time.Time) interface{} {
}
return t.UTC()
}
+
+// CreateProject inserts a new project.
+func (s *DB) CreateProject(p *task.Project) error {
+ now := time.Now().UTC()
+ _, err := s.db.Exec(
+ `INSERT INTO projects (id, name, remote_url, local_path, type, deploy_script, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+ p.ID, p.Name, p.RemoteURL, p.LocalPath, p.Type, p.DeployScript, now, now,
+ )
+ return err
+}
+
+// GetProject retrieves a project by ID.
+func (s *DB) GetProject(id string) (*task.Project, error) {
+ row := s.db.QueryRow(`SELECT id, name, remote_url, local_path, type, deploy_script FROM projects WHERE id = ?`, id)
+ p := &task.Project{}
+ if err := row.Scan(&p.ID, &p.Name, &p.RemoteURL, &p.LocalPath, &p.Type, &p.DeployScript); err != nil {
+ return nil, err
+ }
+ return p, nil
+}
+
+// ListProjects returns all projects.
+func (s *DB) ListProjects() ([]*task.Project, error) {
+ rows, err := s.db.Query(`SELECT id, name, remote_url, local_path, type, deploy_script FROM projects ORDER BY name`)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var projects []*task.Project
+ for rows.Next() {
+ p := &task.Project{}
+ if err := rows.Scan(&p.ID, &p.Name, &p.RemoteURL, &p.LocalPath, &p.Type, &p.DeployScript); err != nil {
+ return nil, err
+ }
+ projects = append(projects, p)
+ }
+ return projects, rows.Err()
+}
+
+// UpdateProject updates an existing project.
+func (s *DB) UpdateProject(p *task.Project) error {
+ now := time.Now().UTC()
+ _, err := s.db.Exec(
+ `UPDATE projects SET name = ?, remote_url = ?, local_path = ?, type = ?, deploy_script = ?, updated_at = ? WHERE id = ?`,
+ p.Name, p.RemoteURL, p.LocalPath, p.Type, p.DeployScript, now, p.ID,
+ )
+ return err
+}
+
+// UpsertProject inserts or updates a project by ID (used for seeding).
+func (s *DB) UpsertProject(p *task.Project) error {
+ now := time.Now().UTC()
+ _, err := s.db.Exec(
+ `INSERT INTO projects (id, name, remote_url, local_path, type, deploy_script, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(id) DO UPDATE SET name=excluded.name, remote_url=excluded.remote_url,
+ local_path=excluded.local_path, type=excluded.type, deploy_script=excluded.deploy_script, updated_at=excluded.updated_at`,
+ p.ID, p.Name, p.RemoteURL, p.LocalPath, p.Type, p.DeployScript, now, now,
+ )
+ return err
+}
diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go
index 5c447af..82c8262 100644
--- a/internal/storage/db_test.go
+++ b/internal/storage/db_test.go
@@ -41,7 +41,6 @@ func TestCreateTask_AndGetTask(t *testing.T) {
Type: "claude",
Model: "sonnet",
Instructions: "do it",
- ProjectDir: "/tmp",
MaxBudgetUSD: 2.5,
},
Priority: task.PriorityHigh,
@@ -1154,3 +1153,68 @@ func TestExecution_StoreAndRetrieveChangestats(t *testing.T) {
}
}
+func TestCreateProject(t *testing.T) {
+ db := testDB(t)
+ defer db.Close()
+
+ p := &task.Project{
+ ID: "proj-1",
+ Name: "claudomator",
+ RemoteURL: "/bare/claudomator.git",
+ LocalPath: "/workspace/claudomator",
+ Type: "web",
+ }
+ if err := db.CreateProject(p); err != nil {
+ t.Fatalf("CreateProject: %v", err)
+ }
+ got, err := db.GetProject("proj-1")
+ if err != nil {
+ t.Fatalf("GetProject: %v", err)
+ }
+ if got.Name != "claudomator" {
+ t.Errorf("Name: want claudomator, got %q", got.Name)
+ }
+ if got.LocalPath != "/workspace/claudomator" {
+ t.Errorf("LocalPath: want /workspace/claudomator, got %q", got.LocalPath)
+ }
+}
+
+func TestListProjects(t *testing.T) {
+ db := testDB(t)
+ defer db.Close()
+
+ for _, p := range []*task.Project{
+ {ID: "p1", Name: "alpha", Type: "web"},
+ {ID: "p2", Name: "beta", Type: "android"},
+ } {
+ if err := db.CreateProject(p); err != nil {
+ t.Fatalf("CreateProject: %v", err)
+ }
+ }
+ list, err := db.ListProjects()
+ if err != nil {
+ t.Fatalf("ListProjects: %v", err)
+ }
+ if len(list) != 2 {
+ t.Errorf("want 2 projects, got %d", len(list))
+ }
+}
+
+func TestUpdateProject(t *testing.T) {
+ db := testDB(t)
+ defer db.Close()
+
+ p := &task.Project{ID: "p1", Name: "original", Type: "web"}
+ if err := db.CreateProject(p); err != nil {
+ t.Fatalf("CreateProject: %v", err)
+ }
+ p.Name = "updated"
+ if err := db.UpdateProject(p); err != nil {
+ t.Fatalf("UpdateProject: %v", err)
+ }
+ got, _ := db.GetProject("p1")
+ if got.Name != "updated" {
+ t.Errorf("Name after update: want updated, got %q", got.Name)
+ }
+}
+
diff --git a/internal/storage/seed.go b/internal/storage/seed.go
new file mode 100644
index 0000000..d1ded8a
--- /dev/null
+++ b/internal/storage/seed.go
@@ -0,0 +1,46 @@
+package storage
+
+import (
+ "os/exec"
+ "strings"
+
+ "github.com/thepeterstone/claudomator/internal/task"
+)
+
+// SeedProjects upserts the default project registry on startup.
+func (s *DB) SeedProjects() error {
+ projects := []*task.Project{
+ {
+ ID: "claudomator",
+ Name: "claudomator",
+ LocalPath: "/workspace/claudomator",
+ RemoteURL: localBareRemote("/workspace/claudomator"),
+ Type: "web",
+ },
+ {
+ ID: "nav",
+ Name: "nav",
+ LocalPath: "/workspace/nav",
+ RemoteURL: localBareRemote("/workspace/nav"),
+ Type: "android",
+ },
+ }
+ for _, p := range projects {
+ if err := s.UpsertProject(p); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// localBareRemote returns the URL of the "local" git remote for dir,
+// falling back to dir itself if the remote is not configured.
+func localBareRemote(dir string) string {
+ out, err := exec.Command("git", "-C", dir, "remote", "get-url", "local").Output()
+ if err == nil {
+ if url := strings.TrimSpace(string(out)); url != "" {
+ return url
+ }
+ }
+ return dir
+}
diff --git a/internal/task/project.go b/internal/task/project.go
new file mode 100644
index 0000000..bd8a4fb
--- /dev/null
+++ b/internal/task/project.go
@@ -0,0 +1,11 @@
+package task
+
+// Project represents a registered codebase that agents can operate on.
+type Project struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ RemoteURL string `json:"remote_url"`
+ LocalPath string `json:"local_path"`
+ Type string `json:"type"` // "web" | "android"
+ DeployScript string `json:"deploy_script"` // optional path or command
+}
diff --git a/internal/task/task.go b/internal/task/task.go
index 465de8b..28d65a5 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -32,16 +32,13 @@ type AgentConfig struct {
Model string `yaml:"model" json:"model"`
ContextFiles []string `yaml:"context_files" json:"context_files"`
Instructions string `yaml:"instructions" json:"instructions"`
- RepositoryURL string `yaml:"repository_url" json:"repository_url"`
ContainerImage string `yaml:"container_image" json:"container_image"`
- ProjectDir string `yaml:"project_dir" json:"project_dir"` // Deprecated: use Task.RepositoryURL
MaxBudgetUSD float64 `yaml:"max_budget_usd" json:"max_budget_usd"`
PermissionMode string `yaml:"permission_mode" json:"permission_mode"`
AllowedTools []string `yaml:"allowed_tools" json:"allowed_tools"`
DisallowedTools []string `yaml:"disallowed_tools" json:"disallowed_tools"`
SystemPromptAppend string `yaml:"system_prompt_append" json:"system_prompt_append"`
AdditionalArgs []string `yaml:"additional_args" json:"additional_args"`
- SkipPlanning bool `yaml:"skip_planning" json:"skip_planning"`
}
diff --git a/internal/task/validator_test.go b/internal/task/validator_test.go
index 657d93f..c0ab986 100644
--- a/internal/task/validator_test.go
+++ b/internal/task/validator_test.go
@@ -12,7 +12,6 @@ func validTask() *Task {
Agent: AgentConfig{
Type: "claude",
Instructions: "do something",
- ProjectDir: "/tmp",
},
Priority: PriorityNormal,
Retry: RetryConfig{MaxAttempts: 1, Backoff: "exponential"},