From 175fbb00751b00fbd187e529fddb907133ea360e Mon Sep 17 00:00:00 2001 From: typebasedio Date: Wed, 22 Apr 2026 19:44:36 -0400 Subject: [PATCH] feat(v0.5): add launch_dir field to TaskMeta Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/workspace/metadata.go | 5 ++- internal/workspace/metadata_test.go | 53 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/internal/workspace/metadata.go b/internal/workspace/metadata.go index 4195410..9fc1437 100644 --- a/internal/workspace/metadata.go +++ b/internal/workspace/metadata.go @@ -12,7 +12,9 @@ import ( // TaskMeta represents the task.yaml schema. // The Type field is v0.3+; older workspaces without this field are treated as "task" -// (see EffectiveType). Field order in this struct is the YAML output order. +// (see EffectiveType). LaunchDir is v0.5+; empty for tasks and pre-v0.5 projects, +// defaults to the project slug for new projects. Field order in this struct is +// the YAML output order. type TaskMeta struct { ID string `yaml:"id"` Slug string `yaml:"slug"` @@ -26,6 +28,7 @@ type TaskMeta struct { Agent string `yaml:"agent"` WorkspacePath string `yaml:"workspace_path"` ArchivedAt *time.Time `yaml:"archived_at"` + LaunchDir string `yaml:"launch_dir,omitempty"` } // EffectiveType returns "task" or "project". An empty or missing Type field diff --git a/internal/workspace/metadata_test.go b/internal/workspace/metadata_test.go index 145979f..0ef1580 100644 --- a/internal/workspace/metadata_test.go +++ b/internal/workspace/metadata_test.go @@ -255,3 +255,56 @@ func TestMetaArchive(t *testing.T) { t.Errorf("ArchivedAt: got %v, want %v", got.ArchivedAt, archiveTime) } } + +func TestMetaLaunchDirRoundTrip(t *testing.T) { + tmp := t.TempDir() + path := filepath.Join(tmp, "task.yaml") + + meta := &TaskMeta{ + ID: "test", + Slug: "demo", + Title: "demo", + CreatedAt: time.Now().UTC().Truncate(time.Second), + UpdatedAt: time.Now().UTC().Truncate(time.Second), + Status: "active", + Category: "projects", + Type: "project", + Mode: "local", + Agent: "claude", + LaunchDir: "demo", + } + if err := WriteMeta(path, meta); err != nil { + t.Fatalf("WriteMeta: %v", err) + } + got, err := ReadMeta(path) + if err != nil { + t.Fatalf("ReadMeta: %v", err) + } + if got.LaunchDir != "demo" { + t.Errorf("LaunchDir round-trip: got %q, want %q", got.LaunchDir, "demo") + } +} + +func TestMetaLaunchDirOmittedWhenEmpty(t *testing.T) { + // Backward compat: a task workspace with no LaunchDir must not serialize + // a launch_dir key (keeps on-disk task.yaml clean for pre-v0.5 shape). + tmp := t.TempDir() + path := filepath.Join(tmp, "task.yaml") + meta := &TaskMeta{ + ID: "t", Slug: "t", Title: "t", + CreatedAt: time.Now().UTC().Truncate(time.Second), + UpdatedAt: time.Now().UTC().Truncate(time.Second), + Status: "active", Category: "general", Type: "task", + Mode: "local", Agent: "claude", + } + if err := WriteMeta(path, meta); err != nil { + t.Fatalf("WriteMeta: %v", err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + if strings.Contains(string(body), "launch_dir") { + t.Errorf("empty LaunchDir must not serialize launch_dir key; got:\n%s", body) + } +}