Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5159846
feat: add tasks tool with dependency management
silvin-lubecki Jan 24, 2026
11add00
feat: add tests and TUI support for tasks tool
silvin-lubecki Jan 24, 2026
ad708d7
chore: use tasks tool in golang_developer agent
silvin-lubecki Jan 24, 2026
67ff0c2
fix: address lint issues in tasks tool
silvin-lubecki Jan 24, 2026
26b45aa
fix(tui): show blocker descriptions instead of IDs in tasks sidebar
silvin-lubecki Jan 25, 2026
7633b5d
feat(tasks): add cross-session persistence
silvin-lubecki Jan 25, 2026
50f8999
feat(tasks): always persist with auto-detected list ID from git repo
silvin-lubecki Jan 26, 2026
5e86780
test(tasks): add tests for worktree and cross-repo task list IDs
silvin-lubecki Jan 26, 2026
2aa86d1
docs: add tasks tool documentation
silvin-lubecki Jan 26, 2026
0be7092
fix(tasks): add handler-level mutex to prevent race conditions
silvin-lubecki Jan 26, 2026
1b1c06e
fix(tasks): add circular dependency check in batch creation and save …
silvin-lubecki Jan 26, 2026
b2f3c6b
fix(tasks): fail fast on load error to prevent data loss
silvin-lubecki Jan 26, 2026
2b66cc6
docs: add tasks tool guidance to golang_developer agent prompt
silvin-lubecki Jan 26, 2026
33e5e79
refactor(tasks): simplify to always-shared singleton with file persis…
silvin-lubecki Jan 26, 2026
3455cd2
docs: update tasks tool documentation for always-shared behavior
silvin-lubecki Jan 26, 2026
e4a2b57
refactor(tasks): replace singleton with dependency injection via regi…
silvin-lubecki Jan 26, 2026
7a96bbb
fix(tasks): address PR review comments
silvin-lubecki Jan 26, 2026
e66f981
fix(fake): prevent panic in StreamCopy when client disconnects
silvin-lubecki Jan 26, 2026
0c9bef0
docs(tasks): clarify task ID format in tool descriptions
silvin-lubecki Jan 27, 2026
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
22 changes: 22 additions & 0 deletions cmd/root/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import (
const (
flagModelsGateway = "models-gateway"
envModelsGateway = "CAGENT_MODELS_GATEWAY"
flagTaskList = "task-list"
envTaskListID = "CAGENT_TASK_LIST_ID"
)

func addRuntimeConfigFlags(cmd *cobra.Command, runConfig *config.RuntimeConfig) {
addGatewayFlags(cmd, runConfig)
addTaskListFlags(cmd, runConfig)
cmd.PersistentFlags().StringSliceVar(&runConfig.EnvFiles, "env-from-file", nil, "Set environment variables from file")
cmd.PersistentFlags().BoolVar(&runConfig.GlobalCodeMode, "code-mode-tools", false, "Provide a single tool to call other tools via Javascript")
cmd.PersistentFlags().StringVar(&runConfig.WorkingDir, "working-dir", "", "Set the working directory for the session (applies to tools and relative paths)")
Expand Down Expand Up @@ -94,3 +97,22 @@ func addGatewayFlags(cmd *cobra.Command, runConfig *config.RuntimeConfig) {
return nil
}
}

func addTaskListFlags(cmd *cobra.Command, runConfig *config.RuntimeConfig) {
cmd.PersistentFlags().StringVar(&runConfig.TaskListID, flagTaskList, "", "Use a persistent task list with the given ID")

persistentPreRunE := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(c *cobra.Command, args []string) error {
// Precedence: CLI flag > environment variable
if runConfig.TaskListID != "" {
logFlagShadowing(os.Getenv(envTaskListID), envTaskListID, flagTaskList)
} else if taskListID := os.Getenv(envTaskListID); taskListID != "" {
runConfig.TaskListID = taskListID
}

if persistentPreRunE != nil {
return persistentPreRunE(c, args)
}
return nil
}
}
50 changes: 50 additions & 0 deletions cmd/root/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,53 @@ func TestCanonize(t *testing.T) {
})
}
}

func TestTaskListLogic(t *testing.T) {
tests := []struct {
name string
env string
args []string
expected string
}{
{
name: "empty_by_default",
expected: "",
},
{
name: "from_env",
env: "my-project",
expected: "my-project",
},
{
name: "from_cli",
args: []string{"--task-list", "cli-project"},
expected: "cli-project",
},
{
name: "cli_overrides_env",
env: "env-project",
args: []string{"--task-list", "cli-project"},
expected: "cli-project",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("CAGENT_TASK_LIST_ID", tt.env)

cmd := &cobra.Command{
RunE: func(*cobra.Command, []string) error {
return nil
},
}
runConfig := config.RuntimeConfig{}
addTaskListFlags(cmd, &runConfig)

cmd.SetArgs(tt.args)
err := cmd.Execute()

require.NoError(t, err)
assert.Equal(t, tt.expected, runConfig.TaskListID)
})
}
}
36 changes: 36 additions & 0 deletions e2e/testdata/shared_tasks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: "2"

agents:
root:
model: openai/gpt-5-mini
description: Coordinator that delegates to specialized agents
instruction: |
You coordinate work using a shared task list.
Use transfer_task to delegate work to sub-agents.
sub_agents:
- backend
- frontend
toolsets:
- type: tasks
shared: true
- type: transfer_task

backend:
model: openai/gpt-5-mini
description: Backend developer
instruction: |
You handle backend tasks.
You share a task list with other agents.
toolsets:
- type: tasks
shared: true

frontend:
model: openai/gpt-5-mini
description: Frontend developer
instruction: |
You handle frontend tasks.
You share a task list with other agents.
toolsets:
- type: tasks
shared: true
16 changes: 16 additions & 0 deletions e2e/testdata/tasks_dependencies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: "2"

agents:
root:
model: openai/gpt-5-mini
description: Test agent for tasks with dependencies
instruction: |
You are a helpful assistant that uses tasks tools with dependencies.

When creating tasks:
- Use blocked_by to specify dependencies
- Use owner to assign tasks

Always use the tasks tools to track work.
toolsets:
- type: tasks
2 changes: 1 addition & 1 deletion golang_developer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ agents:
toolsets:
- type: filesystem
- type: shell
- type: todo
- type: tasks
- type: fetch
sub_agents:
- librarian
Expand Down
1 change: 1 addition & 0 deletions pkg/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config struct {
ModelsGateway string
GlobalCodeMode bool
WorkingDir string
TaskListID string // ID for persistent task list (from --task-list or CAGENT_TASK_LIST_ID)
}

func (runConfig *RuntimeConfig) Clone() *RuntimeConfig {
Expand Down
5 changes: 5 additions & 0 deletions pkg/paths/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ func GetHomeDir() string {
}
return filepath.Clean(homeDir)
}

// GetTasksDir returns the directory for storing task lists.
func GetTasksDir() string {
return filepath.Join(GetDataDir(), "tasks")
}
19 changes: 19 additions & 0 deletions pkg/teamloader/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func NewDefaultToolsetRegistry() *ToolsetRegistry {
r := NewToolsetRegistry()
// Register all built-in toolset creators
r.Register("todo", createTodoTool)
r.Register("tasks", createTasksTool)
r.Register("memory", createMemoryTool)
r.Register("think", createThinkTool)
r.Register("shell", createShellTool)
Expand All @@ -80,6 +81,24 @@ func createTodoTool(_ context.Context, toolset latest.Toolset, _ string, _ *conf
return builtin.NewTodoTool(), nil
}

func createTasksTool(_ context.Context, toolset latest.Toolset, _ string, runConfig *config.RuntimeConfig) (tools.ToolSet, error) {
// Determine task list ID: CLI/env takes precedence
listID := runConfig.TaskListID

if toolset.Shared {
// Shared mode uses in-memory storage (no persistence)
return builtin.NewSharedTasksTool(), nil
}

if listID != "" {
// Use file-based persistence with the specified list ID
return builtin.NewTasksToolWithStore(builtin.NewFileTaskStore(listID)), nil
}

// Default: in-memory only (no persistence)
return builtin.NewTasksTool(), nil
}

func createMemoryTool(_ context.Context, toolset latest.Toolset, parentDir string, runConfig *config.RuntimeConfig) (tools.ToolSet, error) {
var memoryPath string
if filepath.IsAbs(toolset.Path) {
Expand Down
Loading