feat: improve add_tasks output behavior

Amolith created

- Show verbose instructions + task list when no existing tasks
- Show brief task list output when tasks already exist
- Extract shared formatting logic into reusable formatTaskList method
- Update documentation to reflect new behavior

Change summary

AGENTS.md                    |  2 
README.md                    | 31 +++++++++--------------------
internal/mcp/server.go       | 25 ++++++++++++++++-------
internal/planning/manager.go | 39 +++++++++++++++++++++++++++----------
4 files changed, 56 insertions(+), 41 deletions(-)

Detailed changes

AGENTS.md 🔗

@@ -54,7 +54,7 @@ The project requires license headers (SPDX format) on all source files and uses
 
 The server exposes four MCP tools that map directly to planning manager methods:
 - `update_goal(goal: string)`: Sets overarching goal with length validation
-- `add_tasks(tasks: []TaskInput)`: Batch task creation with duplicate detection. Encourages breaking tasks down into smallest units of work and regular progress tracking.
+- `add_tasks(tasks: []TaskInput)`: Batch task creation with duplicate detection. Encourages breaking tasks down into smallest units of work and regular progress tracking. Output behavior depends on existing tasks: shows verbose instructions + task list when no tasks existed previously, shows brief task list (like `get_tasks`) when tasks already existed.
 - `get_tasks()`: Returns markdown-formatted task list with legend and sorted by creation time. Should be called frequently to stay organized.
 - `update_task_status(task_id: string, status: string)`: Updates task status and returns full list. Helps maintain planning workflow by tracking progress.
 

README.md 🔗

@@ -93,28 +93,17 @@ Response: `Goal "Create a comprehensive MCP server for task planning and managem
 
 ### Adding Tasks
 
-```json
-{
-  "name": "add_tasks", 
-  "arguments": {
-    "tasks": [
-      {
-        "title": "Set up project structure",
-        "description": "Create Go module, directories, and basic files"
-      },
-      {
-        "title": "Implement core planning logic",
-        "description": "Create Goal and Task data structures with deterministic IDs"
-      },
-      {
-        "title": "Build MCP server integration"
-      }
-    ]
-  }
-}
-```
+When you first add tasks to an empty planning session, the tool provides guidance and shows your complete plan:
+
+- Adds your tasks to the planning session
+- Shows helpful instructions for getting started
+- Displays your goal and current task list with status indicators
+
+When adding tasks to an existing planning session, the tool keeps things brief:
 
-Response: `Tasks added successfully! Get started on your first one once you're ready, and call get_tasks frequently to remind yourself where you are in the process. Reminder that your overarching goal is "Create a comprehensive MCP server for task planning and management".`
+- Adds your tasks seamlessly 
+- Shows your updated task list (same format as `get_tasks`)
+- No repetitive instructions - just your updated plan
 
 ### Getting Task Status
 

internal/mcp/server.go 🔗

@@ -228,7 +228,8 @@ func (s *Server) handleAddTasks(ctx context.Context, request mcp.CallToolRequest
 	}
 
 	// Add tasks
-	if err := s.planner.AddTasks(tasks); err != nil {
+	result, err := s.planner.AddTasks(tasks)
+	if err != nil {
 		s.logger.Error("Failed to add tasks", "error", err)
 		return &mcp.CallToolResult{
 			Content: []mcp.Content{
@@ -241,14 +242,22 @@ func (s *Server) handleAddTasks(ctx context.Context, request mcp.CallToolRequest
 		}, nil
 	}
 
-	// Get current goal for reminder
-	goal := s.planner.GetGoal()
-	goalText := "your planning session"
-	if goal != nil {
-		goalText = fmt.Sprintf("\"%s\"", goal.Text)
-	}
+	// Get the full task list with goal and legend
+	taskList := s.planner.GetTasks()
 
-	response := fmt.Sprintf("Tasks added successfully! Get started on your first one once you're ready, and call `get_tasks` frequently to remind yourself where you are in the process. Reminder that your overarching goal is %s.", goalText)
+	var response string
+	if !result.HadExistingTasks {
+		// No existing tasks - show verbose instructions + task list
+		goal := s.planner.GetGoal()
+		goalText := "your planning session"
+		if goal != nil {
+			goalText = fmt.Sprintf("\"%s\"", goal.Text)
+		}
+		response = fmt.Sprintf("Tasks added successfully! Get started on your first one once you're ready, and call `get_tasks` frequently to remind yourself where you are in the process. Reminder that your overarching goal is %s.\n\n%s", goalText, taskList)
+	} else {
+		// Had existing tasks - just show the task list (like get_tasks)
+		response = taskList
+	}
 	return &mcp.CallToolResult{
 		Content: []mcp.Content{
 			mcp.TextContent{

internal/planning/manager.go 🔗

@@ -52,14 +52,23 @@ func (m *Manager) UpdateGoal(goalText string) error {
 	return nil
 }
 
+// AddTasksResult contains the result of adding tasks
+type AddTasksResult struct {
+	AddedTasks       []*Task
+	HadExistingTasks bool
+}
+
 // AddTasks adds one or more tasks
-func (m *Manager) AddTasks(tasks []TaskInput) error {
+func (m *Manager) AddTasks(tasks []TaskInput) (*AddTasksResult, error) {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 
+	// Check if there were existing tasks before adding new ones
+	hadExistingTasks := len(m.tasks) > 0
+
 	// Check task limits
 	if len(m.tasks)+len(tasks) > m.config.Planning.MaxTasks {
-		return fmt.Errorf("too many tasks (max %d)", m.config.Planning.MaxTasks)
+		return nil, fmt.Errorf("too many tasks (max %d)", m.config.Planning.MaxTasks)
 	}
 
 	addedTasks := make([]*Task, 0, len(tasks))
@@ -67,15 +76,15 @@ func (m *Manager) AddTasks(tasks []TaskInput) error {
 	for _, taskInput := range tasks {
 		// Validate task input
 		if taskInput.Title == "" {
-			return fmt.Errorf("task title cannot be empty")
+			return nil, fmt.Errorf("task title cannot be empty")
 		}
 
 		if len(taskInput.Title) > m.config.Planning.MaxTaskLength {
-			return fmt.Errorf("task title too long (max %d characters)", m.config.Planning.MaxTaskLength)
+			return nil, fmt.Errorf("task title too long (max %d characters)", m.config.Planning.MaxTaskLength)
 		}
 
 		if len(taskInput.Description) > m.config.Planning.MaxTaskLength {
-			return fmt.Errorf("task description too long (max %d characters)", m.config.Planning.MaxTaskLength)
+			return nil, fmt.Errorf("task description too long (max %d characters)", m.config.Planning.MaxTaskLength)
 		}
 
 		// Create task
@@ -92,14 +101,15 @@ func (m *Manager) AddTasks(tasks []TaskInput) error {
 	}
 
 	m.logger.Info("Tasks added", "count", len(addedTasks))
-	return nil
+	return &AddTasksResult{
+		AddedTasks:       addedTasks,
+		HadExistingTasks: hadExistingTasks,
+	}, nil
 }
 
-// GetTasks returns a markdown-formatted list of tasks
-func (m *Manager) GetTasks() string {
-	m.mu.RLock()
-	defer m.mu.RUnlock()
-
+// formatTaskList returns a markdown-formatted list of tasks
+// This method assumes the mutex is already locked
+func (m *Manager) formatTaskList() string {
 	var lines []string
 
 	// Add goal if it exists
@@ -159,6 +169,13 @@ func (m *Manager) GetTasks() string {
 	return strings.Join(lines, "\n")
 }
 
+// GetTasks returns a markdown-formatted list of tasks
+func (m *Manager) GetTasks() string {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	return m.formatTaskList()
+}
+
 // UpdateTaskStatus updates the status of a specific task
 func (m *Manager) UpdateTaskStatus(taskID string, status TaskStatus) error {
 	m.mu.Lock()