diff --git a/internal/workspace/create.go b/internal/workspace/create.go index 9dae4ad..2c0fd5f 100644 --- a/internal/workspace/create.go +++ b/internal/workspace/create.go @@ -134,6 +134,19 @@ func Create(opts CreateOpts) (*CreateResult, error) { WorkspacePath: wsDir, ArchivedAt: nil, } + if opts.IsProject { + meta.LaunchDir = actualSlug + } + + // v0.5: project workspaces get an empty subdirectory named after the + // final suffixed slug. ctask does not seed any files inside it; the + // user populates it with their project code + their own CLAUDE.md. + if opts.IsProject { + projDir := filepath.Join(wsDir, actualSlug) + if err := os.MkdirAll(projDir, 0755); err != nil { + return nil, fmt.Errorf("creating project subdir: %w", err) + } + } // task.yaml is always written by ctask, AFTER the seed step, so seeds can // never inject a stale or malformed task.yaml. diff --git a/internal/workspace/create_test.go b/internal/workspace/create_test.go index ea00874..52ec018 100644 --- a/internal/workspace/create_test.go +++ b/internal/workspace/create_test.go @@ -309,3 +309,83 @@ func TestCreateCollisionSuffix(t *testing.T) { t.Errorf("collision slug: got %q, want \"same-name-2\"", meta2.Slug) } } + +func TestCreateProjectScaffoldsSubdirAndSetsLaunchDir(t *testing.T) { + root := t.TempDir() + res, err := Create(CreateOpts{ + Root: root, + Title: "litlink-v2", + Category: "projects", + Mode: "local", + Agent: "claude", + IsProject: true, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + subdir := filepath.Join(res.Path, res.Meta.Slug) + info, err := os.Stat(subdir) + if err != nil { + t.Fatalf("project subdir missing: %v", err) + } + if !info.IsDir() { + t.Errorf("project subdir exists but is not a directory") + } + entries, _ := os.ReadDir(subdir) + if len(entries) != 0 { + t.Errorf("project subdir must be empty, got %d entries", len(entries)) + } + if res.Meta.LaunchDir != res.Meta.Slug { + t.Errorf("LaunchDir: got %q, want slug %q", res.Meta.LaunchDir, res.Meta.Slug) + } +} + +func TestCreateProjectSubdirMatchesSuffixedSlug(t *testing.T) { + root := t.TempDir() + first, err := Create(CreateOpts{ + Root: root, Title: "dup", Category: "projects", + Mode: "local", Agent: "claude", IsProject: true, + }) + if err != nil { + t.Fatalf("first Create: %v", err) + } + if first.Meta.Slug != "dup" { + t.Fatalf("first slug: got %q, want %q", first.Meta.Slug, "dup") + } + + second, err := Create(CreateOpts{ + Root: root, Title: "dup", Category: "projects", + Mode: "local", Agent: "claude", IsProject: true, + }) + if err != nil { + t.Fatalf("second Create: %v", err) + } + if second.Meta.Slug == "dup" { + t.Fatalf("expected suffixed slug, got %q", second.Meta.Slug) + } + subdir := filepath.Join(second.Path, second.Meta.Slug) + if _, err := os.Stat(subdir); err != nil { + t.Errorf("subdir %q missing: %v", second.Meta.Slug, err) + } + if second.Meta.LaunchDir != second.Meta.Slug { + t.Errorf("LaunchDir: got %q, want %q", second.Meta.LaunchDir, second.Meta.Slug) + } +} + +func TestCreateTaskDoesNotScaffoldSubdir(t *testing.T) { + root := t.TempDir() + res, err := Create(CreateOpts{ + Root: root, Title: "hello", Category: "general", + Mode: "local", Agent: "claude", IsProject: false, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + subdir := filepath.Join(res.Path, res.Meta.Slug) + if _, err := os.Stat(subdir); err == nil { + t.Errorf("task workspace should not have a slug-named subdir; found %s", subdir) + } + if res.Meta.LaunchDir != "" { + t.Errorf("LaunchDir: got %q, want empty", res.Meta.LaunchDir) + } +}