From 509a6d64ea75baf76509a395d645a4e0411eb07a Mon Sep 17 00:00:00 2001 From: typebasedio Date: Wed, 22 Apr 2026 19:48:23 -0400 Subject: [PATCH] feat(v0.5): export CTASK_LAUNCH_DIR into child sessions config.EnvVars gains a 7th launchDir argument. cmd/new, cmd/resume, and cmd/open pass ws.Meta.LaunchDir. Child sessions can read CTASK_LAUNCH_DIR to know which subdirectory ctask launched them into. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/new.go | 2 +- cmd/open.go | 2 +- cmd/resume.go | 2 +- internal/config/config.go | 17 ++++++++++------- internal/config/config_test.go | 35 +++++++++++++++++++++++++--------- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index 1853674..1e4e182 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -119,7 +119,7 @@ func runNew(cmd *cobra.Command, args []string) error { return nil } - envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta)) + envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta), ws.Meta.LaunchDir) return session.Run(session.LaunchOpts{ WsDir: ws.Path, diff --git a/cmd/open.go b/cmd/open.go index efc35ed..3ed137c 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -42,7 +42,7 @@ func runOpen(cmd *cobra.Command, args []string) error { return fmt.Errorf("updating metadata: %w", err) } - envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, ws.Root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta)) + envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, ws.Root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta), ws.Meta.LaunchDir) return session.Run(session.LaunchOpts{ WsDir: ws.Path, diff --git a/cmd/resume.go b/cmd/resume.go index 11dcab0..f5d5019 100644 --- a/cmd/resume.go +++ b/cmd/resume.go @@ -62,7 +62,7 @@ func doResume(query string, container, useShell, force bool, agentOverride strin agent = ws.Meta.Agent } - envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, ws.Root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta)) + envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, ws.Root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta), ws.Meta.LaunchDir) return session.Run(session.LaunchOpts{ WsDir: ws.Path, diff --git a/internal/config/config.go b/internal/config/config.go index 6c6b1b6..02348c9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,17 +90,20 @@ func samePath(a, b string) bool { // EnvVars returns the environment variables to export into child sessions. // taskType must be "task" or "project"; an empty value defaults to "task". -func EnvVars(slug, mode, root, workspace, category, taskType string) map[string]string { +// launchDir is the workspace-relative project subdirectory for projects, +// or empty for tasks and pre-v0.5 projects. +func EnvVars(slug, mode, root, workspace, category, taskType, launchDir string) map[string]string { if taskType == "" { taskType = "task" } return map[string]string{ - "CTASK_TASK": slug, - "CTASK_MODE": mode, - "CTASK_ROOT": root, - "CTASK_WORKSPACE": workspace, - "CTASK_CATEGORY": category, - "CTASK_TYPE": taskType, + "CTASK_TASK": slug, + "CTASK_MODE": mode, + "CTASK_ROOT": root, + "CTASK_WORKSPACE": workspace, + "CTASK_CATEGORY": category, + "CTASK_TYPE": taskType, + "CTASK_LAUNCH_DIR": launchDir, } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e15b6cc..1814897 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -145,14 +145,15 @@ func TestResolveProjectRootOverride(t *testing.T) { } func TestEnvVars(t *testing.T) { - vars := EnvVars("my-slug", "local", "/abs/root", "/abs/root/cat/ws", "general", "task") + vars := EnvVars("my-slug", "local", "/abs/root", "/abs/root/cat/ws", "general", "task", "") expected := map[string]string{ - "CTASK_TASK": "my-slug", - "CTASK_MODE": "local", - "CTASK_ROOT": "/abs/root", - "CTASK_WORKSPACE": "/abs/root/cat/ws", - "CTASK_CATEGORY": "general", - "CTASK_TYPE": "task", + "CTASK_TASK": "my-slug", + "CTASK_MODE": "local", + "CTASK_ROOT": "/abs/root", + "CTASK_WORKSPACE": "/abs/root/cat/ws", + "CTASK_CATEGORY": "general", + "CTASK_TYPE": "task", + "CTASK_LAUNCH_DIR": "", } for k, v := range expected { if vars[k] != v { @@ -162,15 +163,31 @@ func TestEnvVars(t *testing.T) { } func TestEnvVarsProjectType(t *testing.T) { - vars := EnvVars("p", "local", "/r", "/r/p", "projects", "project") + vars := EnvVars("p", "local", "/r", "/r/p", "projects", "project", "p") if vars["CTASK_TYPE"] != "project" { t.Errorf("CTASK_TYPE: got %q, want \"project\"", vars["CTASK_TYPE"]) } } func TestEnvVarsEmptyTypeDefaultsToTask(t *testing.T) { - vars := EnvVars("p", "local", "/r", "/r/p", "general", "") + vars := EnvVars("p", "local", "/r", "/r/p", "general", "", "") if vars["CTASK_TYPE"] != "task" { t.Errorf("CTASK_TYPE empty fallback: got %q, want \"task\"", vars["CTASK_TYPE"]) } } + +func TestEnvVarsIncludesLaunchDir(t *testing.T) { + env := EnvVars("demo", "local", "/ws", "/ws/2026-04-22_demo", "projects", "project", "demo") + if got := env["CTASK_LAUNCH_DIR"]; got != "demo" { + t.Errorf("CTASK_LAUNCH_DIR: got %q, want %q", got, "demo") + } +} + +func TestEnvVarsLaunchDirEmpty(t *testing.T) { + env := EnvVars("demo", "local", "/ws", "/ws/2026-04-22_demo", "general", "task", "") + if got, ok := env["CTASK_LAUNCH_DIR"]; !ok { + t.Errorf("CTASK_LAUNCH_DIR should be set (empty value), got absent") + } else if got != "" { + t.Errorf("CTASK_LAUNCH_DIR: got %q, want empty", got) + } +}