feat(mcp): Add tagInfo to responses and integrate ContextGatherer

Enhances the MCP server to include 'tagInfo' (currentTag, availableTags) in all tool responses, providing better client-side context.

- Introduces a new 'ContextGatherer' utility to standardize the collection of file, task, and project context for AI-powered commands. This refactors several task-manager modules ('expand-task', 'research', 'update-task', etc.) to use the new utility.

- Fixes an issue in 'get-task' and 'get-tasks' MCP tools where the 'projectRoot' was not being passed correctly, preventing tag information from being included in their responses.

- Adds subtask '103.17' to track the implementation of the task template importing feature.

- Updates documentation ('.cursor/rules', 'docs/') to align with the new tagged task system and context gatherer logic.
This commit is contained in:
Eyal Toledano
2025-06-11 23:06:36 -04:00
parent bb775e3180
commit 83d6405b17
29 changed files with 34433 additions and 13239 deletions

View File

@@ -20,19 +20,21 @@ alwaysApply: false
- **[`task-manager.js`](mdc:scripts/modules/task-manager.js) & `task-manager/` directory: Task Data & Core Logic** - **[`task-manager.js`](mdc:scripts/modules/task-manager.js) & `task-manager/` directory: Task Data & Core Logic**
- **Purpose**: Contains core functions for task data manipulation (CRUD), AI interactions, and related logic. - **Purpose**: Contains core functions for task data manipulation (CRUD), AI interactions, and related logic.
- **Responsibilities**: - **Responsibilities**:
- Reading/writing `tasks.json`. - Reading/writing `tasks.json` with tagged task lists support.
- Implementing functions for task CRUD, parsing PRDs, expanding tasks, updating status, etc. - Implementing functions for task CRUD, parsing PRDs, expanding tasks, updating status, etc.
- **Tagged Task Lists**: Handles task organization across multiple contexts (tags) like "master", branch names, or project phases.
- **Tag Resolution**: Provides backward compatibility by resolving tagged format to legacy format transparently.
- **Delegating AI interactions** to the `ai-services-unified.js` layer. - **Delegating AI interactions** to the `ai-services-unified.js` layer.
- Accessing non-AI configuration via `config-manager.js` getters. - Accessing non-AI configuration via `config-manager.js` getters.
- **Key Files**: Individual files within `scripts/modules/task-manager/` handle specific actions (e.g., `add-task.js`, `expand-task.js`). - **Key Files**: Individual files within `scripts/modules/task-manager/` handle specific actions (e.g., `add-task.js`, `expand-task.js`).
- **[`dependency-manager.js`](mdc:scripts/modules/dependency-manager.js): Dependency Management** - **[`dependency-manager.js`](mdc:scripts/modules/dependency-manager.js): Dependency Management**
- **Purpose**: Manages task dependencies. - **Purpose**: Manages task dependencies.
- **Responsibilities**: Add/remove/validate/fix dependencies. - **Responsibilities**: Add/remove/validate/fix dependencies across tagged task contexts.
- **[`ui.js`](mdc:scripts/modules/ui.js): User Interface Components** - **[`ui.js`](mdc:scripts/modules/ui.js): User Interface Components**
- **Purpose**: Handles CLI output formatting (tables, colors, boxes, spinners). - **Purpose**: Handles CLI output formatting (tables, colors, boxes, spinners).
- **Responsibilities**: Displaying tasks, reports, progress, suggestions. - **Responsibilities**: Displaying tasks, reports, progress, suggestions, and migration notices for tagged systems.
- **[`ai-services-unified.js`](mdc:scripts/modules/ai-services-unified.js): Unified AI Service Layer** - **[`ai-services-unified.js`](mdc:scripts/modules/ai-services-unified.js): Unified AI Service Layer**
- **Purpose**: Centralized interface for all LLM interactions using Vercel AI SDK. - **Purpose**: Centralized interface for all LLM interactions using Vercel AI SDK.
@@ -53,6 +55,7 @@ alwaysApply: false
- **Responsibilities** (See also: [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc)): - **Responsibilities** (See also: [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc)):
- Reads and merges `.taskmasterconfig` with defaults. - Reads and merges `.taskmasterconfig` with defaults.
- Provides getters (e.g., `getMainProvider`, `getLogLevel`, `getDefaultSubtasks`) for accessing settings. - Provides getters (e.g., `getMainProvider`, `getLogLevel`, `getDefaultSubtasks`) for accessing settings.
- **Tag Configuration**: Manages `global.defaultTag` and `tags` section for tag system settings.
- **Note**: Does **not** store or directly handle API keys (keys are in `.env` or MCP `session.env`). - **Note**: Does **not** store or directly handle API keys (keys are in `.env` or MCP `session.env`).
- **[`utils.js`](mdc:scripts/modules/utils.js): Core Utility Functions** - **[`utils.js`](mdc:scripts/modules/utils.js): Core Utility Functions**
@@ -62,6 +65,8 @@ alwaysApply: false
- Task utils (`findTaskById`), Dependency utils (`findCycles`). - Task utils (`findTaskById`), Dependency utils (`findCycles`).
- API Key Resolution (`resolveEnvVariable`). - API Key Resolution (`resolveEnvVariable`).
- Silent Mode Control (`enableSilentMode`, `disableSilentMode`). - Silent Mode Control (`enableSilentMode`, `disableSilentMode`).
- **Tagged Task Lists**: Silent migration system, tag resolution, current tag management.
- **Migration System**: `performCompleteTagMigration`, `migrateConfigJson`, `createStateJson`.
- **[`mcp-server/`](mdc:mcp-server/): MCP Server Integration** - **[`mcp-server/`](mdc:mcp-server/): MCP Server Integration**
- **Purpose**: Provides MCP interface using FastMCP. - **Purpose**: Provides MCP interface using FastMCP.
@@ -71,16 +76,42 @@ alwaysApply: false
- Tool `execute` methods call **direct function wrappers** (`mcp-server/src/core/direct-functions/*.js`), passing the normalized `projectRoot` and other args. - Tool `execute` methods call **direct function wrappers** (`mcp-server/src/core/direct-functions/*.js`), passing the normalized `projectRoot` and other args.
- Direct functions use path utilities (`mcp-server/src/core/utils/`) to resolve paths based on `projectRoot` from session. - Direct functions use path utilities (`mcp-server/src/core/utils/`) to resolve paths based on `projectRoot` from session.
- Direct functions implement silent mode, logger wrappers, and call core logic functions from `scripts/modules/`. - Direct functions implement silent mode, logger wrappers, and call core logic functions from `scripts/modules/`.
- **Tagged Task Lists**: MCP tools need updating to handle tagged format (Part 2 implementation).
- Manages MCP caching and response formatting. - Manages MCP caching and response formatting.
- **[`init.js`](mdc:scripts/init.js): Project Initialization Logic** - **[`init.js`](mdc:scripts/init.js): Project Initialization Logic**
- **Purpose**: Sets up new Task Master project structure. - **Purpose**: Sets up new Task Master project structure.
- **Responsibilities**: Creates directories, copies templates, manages `package.json`, sets up `.cursor/mcp.json`. - **Responsibilities**: Creates directories, copies templates, manages `package.json`, sets up `.cursor/mcp.json`, initializes state.json for tagged system.
## Tagged Task Lists System Architecture
**Data Structure**: Task Master now uses a tagged task lists system where the `tasks.json` file contains multiple named task lists as top-level keys:
```json
{
"master": {
"tasks": [/* standard task objects */]
},
"feature-branch": {
"tasks": [/* separate task context */]
}
}
```
**Key Components:**
- **Silent Migration**: Automatically transforms legacy `{"tasks": [...]}` format to tagged format `{"master": {"tasks": [...]}}` on first read
- **Tag Resolution Layer**: Provides 100% backward compatibility by intercepting tagged format and returning legacy format to existing code
- **Configuration Integration**: `global.defaultTag` and `tags` section in config.json manage tag system settings
- **State Management**: `.taskmaster/state.json` tracks current tag, migration status, and tag-branch mappings
- **Migration Notice**: User-friendly notification system for seamless migration experience
**Backward Compatibility**: All existing CLI commands and MCP tools continue to work unchanged. The tag resolution layer ensures that existing code receives the expected legacy format while the underlying storage uses the new tagged structure.
- **Data Flow and Module Dependencies (Updated)**: - **Data Flow and Module Dependencies (Updated)**:
- **CLI**: `bin/task-master.js` -> `scripts/dev.js` (loads `.env`) -> `scripts/modules/commands.js` -> Core Logic (`scripts/modules/*`) -> Unified AI Service (`ai-services-unified.js`) -> Provider Adapters -> LLM API. - **CLI**: `bin/task-master.js` -> `scripts/dev.js` (loads `.env`) -> `scripts/modules/commands.js` -> Core Logic (`scripts/modules/*`) -> **Tag Resolution Layer** -> Unified AI Service (`ai-services-unified.js`) -> Provider Adapters -> LLM API.
- **MCP**: External Tool -> `mcp-server/server.js` -> Tool (`mcp-server/src/tools/*`) -> Direct Function (`mcp-server/src/core/direct-functions/*`) -> Core Logic (`scripts/modules/*`) -> Unified AI Service (`ai-services-unified.js`) -> Provider Adapters -> LLM API. - **MCP**: External Tool -> `mcp-server/server.js` -> Tool (`mcp-server/src/tools/*`) -> Direct Function (`mcp-server/src/core/direct-functions/*`) -> Core Logic (`scripts/modules/*`) -> **Tag Resolution Layer** -> Unified AI Service (`ai-services-unified.js`) -> Provider Adapters -> LLM API.
- **Configuration**: Core logic needing non-AI settings calls `config-manager.js` getters (passing `session.env` via `explicitRoot` if from MCP). Unified AI Service internally calls `config-manager.js` getters (using `role`) for AI params and `utils.js` (`resolveEnvVariable` with `session.env`) for API keys. - **Configuration**: Core logic needing non-AI settings calls `config-manager.js` getters (passing `session.env` via `explicitRoot` if from MCP). Unified AI Service internally calls `config-manager.js` getters (using `role`) for AI params and `utils.js` (`resolveEnvVariable` with `session.env`) for API keys.
## Silent Mode Implementation Pattern in MCP Direct Functions ## Silent Mode Implementation Pattern in MCP Direct Functions
@@ -197,6 +228,7 @@ By following these patterns consistently, direct functions will properly manage
- **Integration Tests**: Located in `tests/integration/`, test interactions between modules - **Integration Tests**: Located in `tests/integration/`, test interactions between modules
- **End-to-End Tests**: Located in `tests/e2e/`, test complete workflows from a user perspective - **End-to-End Tests**: Located in `tests/e2e/`, test complete workflows from a user perspective
- **Test Fixtures**: Located in `tests/fixtures/`, provide reusable test data - **Test Fixtures**: Located in `tests/fixtures/`, provide reusable test data
- **Tagged System Tests**: Test migration, tag resolution, and multi-context functionality
- **Module Design for Testability**: - **Module Design for Testability**:
- **Explicit Dependencies**: Functions accept their dependencies as parameters rather than using globals - **Explicit Dependencies**: Functions accept their dependencies as parameters rather than using globals
@@ -205,12 +237,14 @@ By following these patterns consistently, direct functions will properly manage
- **Clear Module Interfaces**: Each module has well-defined exports that can be mocked in tests - **Clear Module Interfaces**: Each module has well-defined exports that can be mocked in tests
- **Callback Isolation**: Callbacks are defined as separate functions for easier testing - **Callback Isolation**: Callbacks are defined as separate functions for easier testing
- **Stateless Design**: Modules avoid maintaining internal state where possible - **Stateless Design**: Modules avoid maintaining internal state where possible
- **Tag Resolution Testing**: Test both tagged and legacy format handling
- **Mock Integration Patterns**: - **Mock Integration Patterns**:
- **External Libraries**: Libraries like `fs`, `commander`, and `@anthropic-ai/sdk` are mocked at module level - **External Libraries**: Libraries like `fs`, `commander`, and `@anthropic-ai/sdk` are mocked at module level
- **Internal Modules**: Application modules are mocked with appropriate spy functions - **Internal Modules**: Application modules are mocked with appropriate spy functions
- **Testing Function Callbacks**: Callbacks are extracted from mock call arguments and tested in isolation - **Testing Function Callbacks**: Callbacks are extracted from mock call arguments and tested in isolation
- **UI Elements**: Output functions from `ui.js` are mocked to verify display calls - **UI Elements**: Output functions from `ui.js` are mocked to verify display calls
- **Tagged Data Mocking**: Test both legacy and tagged task data structures
- **Testing Flow**: - **Testing Flow**:
- Module dependencies are mocked (following Jest's hoisting behavior) - Module dependencies are mocked (following Jest's hoisting behavior)
@@ -218,6 +252,7 @@ By following these patterns consistently, direct functions will properly manage
- Spy functions are set up on module methods - Spy functions are set up on module methods
- Tests call the functions under test and verify behavior - Tests call the functions under test and verify behavior
- Mocks are reset between test cases to maintain isolation - Mocks are reset between test cases to maintain isolation
- Tagged system behavior is tested for both migration and normal operation
- **Benefits of this Architecture**: - **Benefits of this Architecture**:
@@ -226,8 +261,11 @@ By following these patterns consistently, direct functions will properly manage
- **Mocking Support**: The clear dependency boundaries make mocking straightforward - **Mocking Support**: The clear dependency boundaries make mocking straightforward
- **Test Isolation**: Each component can be tested without affecting others - **Test Isolation**: Each component can be tested without affecting others
- **Callback Testing**: Function callbacks can be extracted and tested independently - **Callback Testing**: Function callbacks can be extracted and tested independently
- **Multi-Context Testing**: Tagged system enables testing different task contexts independently
- **Reusability**: Utility functions and UI components can be reused across different parts of the application. - **Reusability**: Utility functions and UI components can be reused across different parts of the application.
- **Scalability**: New features can be added as new modules or by extending existing ones without significantly impacting other parts of the application. - **Scalability**: New features can be added as new modules or by extending existing ones without significantly impacting other parts of the application.
- **Multi-Context Support**: Tagged task lists enable working across different contexts (branches, environments, phases) without conflicts.
- **Backward Compatibility**: Seamless migration and tag resolution ensure existing workflows continue unchanged.
- **Clarity**: The modular structure provides a clear separation of concerns, making the codebase easier to navigate and understand for developers. - **Clarity**: The modular structure provides a clear separation of concerns, making the codebase easier to navigate and understand for developers.
This architectural overview should help AI models understand the structure and organization of the Task Master CLI codebase, enabling them to more effectively assist with code generation, modification, and understanding. This architectural overview should help AI models understand the structure and organization of the Task Master CLI codebase, enabling them to more effectively assist with code generation, modification, and understanding.
@@ -249,6 +287,7 @@ Follow these steps to add MCP support for an existing Task Master command (see [
- Call core logic. - Call core logic.
- Return `{ success: true/false, data/error, fromCache: boolean }`. - Return `{ success: true/false, data/error, fromCache: boolean }`.
- Export the wrapper function. - Export the wrapper function.
- **Note**: Tag-aware MCP tools will be implemented in Part 2 of the tagged system.
3. **Update `task-master-core.js` with Import/Export**: Add imports/exports for the new `*Direct` function. 3. **Update `task-master-core.js` with Import/Export**: Add imports/exports for the new `*Direct` function.
@@ -275,12 +314,8 @@ The `initialize_project` command provides a way to set up a new Task Master proj
- **MCP Tool**: `initialize_project` - **MCP Tool**: `initialize_project`
- **Functionality**: - **Functionality**:
- Creates necessary directories and files for a new project - Creates necessary directories and files for a new project
- Sets up `tasks.json` and initial task files - Sets up `tasks.json` with tagged structure and initial task files
- Configures project metadata (name, description, version)
- Handles shell alias creation if requested
- Works in both interactive and non-interactive modes
- Creates necessary directories and files for a new project
- Sets up `tasks.json` and initial task files
- Configures project metadata (name, description, version) - Configures project metadata (name, description, version)
- Initializes state.json for tag system
- Handles shell alias creation if requested - Handles shell alias creation if requested
- Works in both interactive and non-interactive modes - Works in both interactive and non-interactive modes

View File

@@ -18,6 +18,7 @@ Task Master offers two primary ways to interact:
- Refer to [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details on the MCP architecture and available tools. - Refer to [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details on the MCP architecture and available tools.
- A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc). - A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc).
- **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change. - **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
- **Note**: MCP tools will be updated for tagged task lists support in Part 2 of the implementation.
2. **`task-master` CLI (For Users & Fallback)**: 2. **`task-master` CLI (For Users & Fallback)**:
- The global `task-master` command provides a user-friendly interface for direct terminal interaction. - The global `task-master` command provides a user-friendly interface for direct terminal interaction.
@@ -25,10 +26,43 @@ Task Master offers two primary ways to interact:
- Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`. - Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
- The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`). - The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
- Refer to [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc) for a detailed command reference. - Refer to [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc) for a detailed command reference.
- **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration.
## Tagged Task Lists System
Task Master now supports **tagged task lists** for multi-context task management:
- **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0"
- **Seamless Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption
- **Backward Compatibility**: All existing commands continue to work exactly as before
- **Context Isolation**: Tasks in different tags are completely separate and isolated
- **Silent Migration**: The first time you run any Task Master command, your existing tasks.json will be automatically migrated to the new tagged format
- **Migration Notice**: You'll see a friendly FYI notice after migration explaining the new system
**Migration Example**:
```json
// Before (legacy format)
{
"tasks": [
{ "id": 1, "title": "Setup API", ... }
]
}
// After (tagged format - automatic)
{
"master": {
"tasks": [
{ "id": 1, "title": "Setup API", ... }
]
}
}
```
**Coming in Part 2**: CLI commands for tag management (`add-tag`, `use-tag`, `list-tags`) and enhanced MCP support.
## Standard Development Workflow Process ## Standard Development Workflow Process
- Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to generate initial tasks.json - Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to generate initial tasks.json with tagged structure
- Begin coding sessions with `get_tasks` / `task-master list` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to see current tasks, status, and IDs - Begin coding sessions with `get_tasks` / `task-master list` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to see current tasks, status, and IDs
- Determine the next task to work on using `next_task` / `task-master next` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)). - Determine the next task to work on using `next_task` / `task-master next` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
- Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before breaking down tasks - Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before breaking down tasks
@@ -107,9 +141,10 @@ Taskmaster configuration is managed through two main mechanisms:
1. **`.taskmaster/config.json` File (Primary):** 1. **`.taskmaster/config.json` File (Primary):**
* Located in the project root directory. * Located in the project root directory.
* Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc. * Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
* **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration.
* **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing. * **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
* **View/Set specific models via `task-master models` command or `models` MCP tool.** * **View/Set specific models via `task-master models` command or `models` MCP tool.**
* Created automatically when you run `task-master models --setup` for the first time. * Created automatically when you run `task-master models --setup` for the first time or during tagged system migration.
2. **Environment Variables (`.env` / `mcp.json`):** 2. **Environment Variables (`.env` / `mcp.json`):**
* Used **only** for sensitive API keys and specific endpoint URLs. * Used **only** for sensitive API keys and specific endpoint URLs.
@@ -117,6 +152,11 @@ Taskmaster configuration is managed through two main mechanisms:
* For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`. * For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`.
* Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`). * Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`).
3. **`.taskmaster/state.json` File (Tagged System State):**
* Tracks current tag context, migration status, and tag-branch mappings.
* Automatically created during tagged system migration.
* Contains: `currentTag`, `lastSwitched`, `branchTagMapping`, `migrationNoticeShown`.
**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool. **Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`. **If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`.
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project. **If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.

View File

@@ -3,7 +3,6 @@ description: Git workflow integrated with Task Master for feature development an
globs: "**/*" globs: "**/*"
alwaysApply: true alwaysApply: true
--- ---
# Git Workflow with Task Master Integration # Git Workflow with Task Master Integration
## **Branch Strategy** ## **Branch Strategy**
@@ -27,6 +26,26 @@ fix-database-issue
random-branch-name random-branch-name
``` ```
## **Tagged Task Lists Integration**
Task Master's **tagged task lists system** provides significant benefits for Git workflows:
### **Multi-Context Development**
- **Branch-Specific Tasks**: Each branch can have its own task context using tags
- **Merge Conflict Prevention**: Tasks in different tags are completely isolated
- **Context Switching**: Seamlessly switch between different development contexts
- **Parallel Development**: Multiple team members can work on separate task contexts
### **Migration and Compatibility**
- **Seamless Migration**: Existing projects automatically migrate to use a "master" tag
- **Zero Disruption**: All existing Git workflows continue unchanged
- **Backward Compatibility**: Legacy projects work exactly as before
### **Future Enhancements (Part 2)**
- **Automatic Tag Creation**: Create tags based on git branch names
- **Branch-Tag Mapping**: Automatically switch tag contexts with branch changes
- **Git Integration**: Enhanced git branch integration features
## **Workflow Overview** ## **Workflow Overview**
```mermaid ```mermaid
@@ -34,7 +53,7 @@ flowchart TD
A[Start: On main branch] --> B[Pull latest changes] A[Start: On main branch] --> B[Pull latest changes]
B --> C[Create task branch<br/>git checkout -b task-XXX] B --> C[Create task branch<br/>git checkout -b task-XXX]
C --> D[Set task status: in-progress] C --> D[Set task status: in-progress]
D --> E[Get task context & expand if needed] D --> E[Get task context & expand if needed<br/>Tasks automatically use current tag]
E --> F[Identify next subtask] E --> F[Identify next subtask]
F --> G[Set subtask: in-progress] F --> G[Set subtask: in-progress]
@@ -79,13 +98,13 @@ git branch # Verify you're on main
# 3. Create task-specific branch # 3. Create task-specific branch
git checkout -b task-004 # For Task 4 git checkout -b task-004 # For Task 4
# 4. Set task status in Task Master # 4. Set task status in Task Master (tasks automatically use current tag context)
# Use: set_task_status tool or `task-master set-status --id=4 --status=in-progress` # Use: set_task_status tool or `task-master set-status --id=4 --status=in-progress`
``` ```
### **Phase 2: Task Analysis & Planning** ### **Phase 2: Task Analysis & Planning**
```bash ```bash
# 5. Get task context and expand if needed # 5. Get task context and expand if needed (uses current tag automatically)
# Use: get_task tool or `task-master show 4` # Use: get_task tool or `task-master show 4`
# Use: expand_task tool or `task-master expand --id=4 --research --force` (if complex) # Use: expand_task tool or `task-master expand --id=4 --research --force` (if complex)
@@ -215,7 +234,7 @@ Task 4: Setup Express.js Server Project - Testing complete"
### **Essential Commands for Git Workflow** ### **Essential Commands for Git Workflow**
```bash ```bash
# Task management # Task management (uses current tag context automatically)
task-master show <id> # Get task/subtask details task-master show <id> # Get task/subtask details
task-master next # Find next task to work on task-master next # Find next task to work on
task-master set-status --id=<id> --status=<status> task-master set-status --id=<id> --status=<status>
@@ -302,16 +321,17 @@ Mention any dependent tasks or follow-up work needed.
## **Conflict Resolution** ## **Conflict Resolution**
### **Task Conflicts** ### **Task Conflicts with Tagged System**
```bash ```bash
# If multiple people work on overlapping tasks: # With tagged task lists, merge conflicts are significantly reduced:
# 1. Use Task Master's move functionality to reorganize # 1. Different branches can use different tag contexts
task-master move --from=5 --to=25 # Move conflicting task # 2. Tasks in separate tags are completely isolated
# 3. Use Task Master's move functionality to reorganize if needed
# 2. Update task dependencies # Future (Part 2): Enhanced git integration
task-master add-dependency --id=6 --depends-on=5 # - Automatic tag creation based on branch names
# - Branch-tag mapping for seamless context switching
# 3. Coordinate through PR comments and task updates # - Git hooks for automated tag management
``` ```
### **Code Conflicts** ### **Code Conflicts**
@@ -359,6 +379,23 @@ git push origin --delete task-<id>
task-master update-task --id=<id> --prompt="Task cancelled due to..." task-master update-task --id=<id> --prompt="Task cancelled due to..."
``` ```
## **Tagged System Benefits for Git Workflows**
### **Multi-Team Development**
- **Isolated Contexts**: Different teams can work on separate tag contexts without conflicts
- **Feature Branches**: Each feature branch can have its own task context
- **Release Management**: Separate tags for different release versions or environments
### **Merge Conflict Prevention**
- **Context Separation**: Tasks in different tags don't interfere with each other
- **Clean Merges**: Reduced likelihood of task-related merge conflicts
- **Parallel Development**: Multiple developers can work simultaneously without task conflicts
### **Future Git Integration (Part 2)**
- **Branch-Tag Mapping**: Automatic tag switching based on git branch
- **Git Hooks**: Automated tag management during branch operations
- **Enhanced Workflow**: Seamless integration between git branches and task contexts
--- ---
**References:** **References:**

View File

@@ -7,20 +7,20 @@ alwaysApply: true
This file provides a quick reference to the purpose of each rule file located in the `.cursor/rules` directory. This file provides a quick reference to the purpose of each rule file located in the `.cursor/rules` directory.
- **[`architecture.mdc`](mdc:.cursor/rules/architecture.mdc)**: Describes the high-level architecture of the Task Master CLI application. - **[`architecture.mdc`](mdc:.cursor/rules/architecture.mdc)**: Describes the high-level architecture of the Task Master CLI application, including the new tagged task lists system.
- **[`changeset.mdc`](mdc:.cursor/rules/changeset.mdc)**: Guidelines for using Changesets (npm run changeset) to manage versioning and changelogs. - **[`changeset.mdc`](mdc:.cursor/rules/changeset.mdc)**: Guidelines for using Changesets (npm run changeset) to manage versioning and changelogs.
- **[`commands.mdc`](mdc:.cursor/rules/commands.mdc)**: Guidelines for implementing CLI commands using Commander.js. - **[`commands.mdc`](mdc:.cursor/rules/commands.mdc)**: Guidelines for implementing CLI commands using Commander.js.
- **[`cursor_rules.mdc`](mdc:.cursor/rules/cursor_rules.mdc)**: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness. - **[`cursor_rules.mdc`](mdc:.cursor/rules/cursor_rules.mdc)**: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness.
- **[`dependencies.mdc`](mdc:.cursor/rules/dependencies.mdc)**: Guidelines for managing task dependencies and relationships. - **[`dependencies.mdc`](mdc:.cursor/rules/dependencies.mdc)**: Guidelines for managing task dependencies and relationships across tagged task contexts.
- **[`dev_workflow.mdc`](mdc:.cursor/rules/dev_workflow.mdc)**: Guide for using Task Master to manage task-driven development workflows. - **[`dev_workflow.mdc`](mdc:.cursor/rules/dev_workflow.mdc)**: Guide for using Task Master to manage task-driven development workflows with tagged task lists support.
- **[`glossary.mdc`](mdc:.cursor/rules/glossary.mdc)**: This file; provides a glossary of other Cursor rules. - **[`glossary.mdc`](mdc:.cursor/rules/glossary.mdc)**: This file; provides a glossary of other Cursor rules.
- **[`mcp.mdc`](mdc:.cursor/rules/mcp.mdc)**: Guidelines for implementing and interacting with the Task Master MCP Server. - **[`mcp.mdc`](mdc:.cursor/rules/mcp.mdc)**: Guidelines for implementing and interacting with the Task Master MCP Server.
- **[`new_features.mdc`](mdc:.cursor/rules/new_features.mdc)**: Guidelines for integrating new features into the Task Master CLI. - **[`new_features.mdc`](mdc:.cursor/rules/new_features.mdc)**: Guidelines for integrating new features into the Task Master CLI with tagged system considerations.
- **[`self_improve.mdc`](mdc:.cursor/rules/self_improve.mdc)**: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices. - **[`self_improve.mdc`](mdc:.cursor/rules/self_improve.mdc)**: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices.
- **[`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)**: Comprehensive reference for Taskmaster MCP tools and CLI commands. - **[`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)**: Comprehensive reference for Taskmaster MCP tools and CLI commands with tagged task lists information.
- **[`tasks.mdc`](mdc:.cursor/rules/tasks.mdc)**: Guidelines for implementing task management operations. - **[`tasks.mdc`](mdc:.cursor/rules/tasks.mdc)**: Guidelines for implementing task management operations with tagged task lists system support.
- **[`tests.mdc`](mdc:.cursor/rules/tests.mdc)**: Guidelines for implementing and maintaining tests for Task Master CLI. - **[`tests.mdc`](mdc:.cursor/rules/tests.mdc)**: Guidelines for implementing and maintaining tests for Task Master CLI.
- **[`ui.mdc`](mdc:.cursor/rules/ui.mdc)**: Guidelines for implementing and maintaining user interface components. - **[`ui.mdc`](mdc:.cursor/rules/ui.mdc)**: Guidelines for implementing and maintaining user interface components.
- **[`utilities.mdc`](mdc:.cursor/rules/utilities.mdc)**: Guidelines for implementing utility functions. - **[`utilities.mdc`](mdc:.cursor/rules/utilities.mdc)**: Guidelines for implementing utility functions including tagged task lists utilities.
- **[`telemetry.mdc`](mdc:.cursor/rules/telemetry.mdc)**: Guidelines for integrating AI usage telemetry across Task Master. - **[`telemetry.mdc`](mdc:.cursor/rules/telemetry.mdc)**: Guidelines for integrating AI usage telemetry across Task Master.

View File

@@ -634,3 +634,287 @@ When implementing project initialization commands:
}); });
} }
``` ```
## Feature Planning
- **Core Logic First**:
- ✅ DO: Implement core logic in `scripts/modules/` before CLI or MCP interfaces
- ✅ DO: Consider tagged task lists system compatibility from the start
- ✅ DO: Design functions to work with both legacy and tagged data formats
- ✅ DO: Use tag resolution functions (`getTasksForTag`, `setTasksForTag`) for task data access
- ❌ DON'T: Directly manipulate tagged data structure in new features
```javascript
// ✅ DO: Design tagged-aware core functions
async function newFeatureCore(tasksPath, featureParams, options = {}) {
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
// Perform feature logic on tasks array
const result = performFeatureLogic(tasks, featureParams);
// Save back using tag resolution
setTasksForTag(tasksData, currentTag, tasks);
writeJSON(tasksPath, tasksData);
return result;
}
```
- **Backward Compatibility**:
- ✅ DO: Ensure new features work with existing projects seamlessly
- ✅ DO: Test with both legacy and tagged task data formats
- ✅ DO: Support silent migration during feature usage
- ❌ DON'T: Break existing workflows when adding tagged system features
## CLI Command Implementation
- **Command Structure**:
- ✅ DO: Follow the established pattern in [`commands.js`](mdc:scripts/modules/commands.js)
- ✅ DO: Use Commander.js for argument parsing
- ✅ DO: Include comprehensive help text and examples
- ✅ DO: Support tagged task context awareness
```javascript
// ✅ DO: Implement CLI commands with tagged system awareness
program
.command('new-feature')
.description('Description of the new feature with tagged task lists support')
.option('-t, --tag <tag>', 'Specify tag context (defaults to current tag)')
.option('-p, --param <value>', 'Feature-specific parameter')
.option('--force', 'Force operation without confirmation')
.action(async (options) => {
try {
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.error('Not in a Task Master project directory');
process.exit(1);
}
// Use specified tag or current tag
const targetTag = options.tag || getCurrentTag() || 'master';
const result = await newFeatureCore(
path.join(projectRoot, '.taskmaster', 'tasks', 'tasks.json'),
{ param: options.param },
{
force: options.force,
targetTag: targetTag,
outputFormat: 'text'
}
);
console.log('Feature executed successfully');
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
```
- **Error Handling**:
- ✅ DO: Provide clear error messages for common failures
- ✅ DO: Handle tagged system migration errors gracefully
- ✅ DO: Include suggestion for resolution when possible
- ✅ DO: Exit with appropriate codes for scripting
## MCP Tool Implementation
- **Direct Function Pattern**:
- ✅ DO: Create direct function wrappers in `mcp-server/src/core/direct-functions/`
- ✅ DO: Follow silent mode patterns to prevent console output interference
- ✅ DO: Use `findTasksJsonPath` for consistent path resolution
- ✅ DO: Ensure tagged system compatibility
```javascript
// ✅ DO: Implement MCP direct functions with tagged awareness
export async function newFeatureDirect(args, log, context = {}) {
try {
const tasksPath = findTasksJsonPath(args, log);
// Enable silent mode for clean MCP responses
enableSilentMode();
try {
const result = await newFeatureCore(
tasksPath,
{ param: args.param },
{
force: args.force,
targetTag: args.tag || 'master', // Support tag specification
mcpLog: log,
session: context.session,
outputFormat: 'json'
}
);
return {
success: true,
data: result,
fromCache: false
};
} finally {
disableSilentMode();
}
} catch (error) {
log.error(`Error in newFeatureDirect: ${error.message}`);
return {
success: false,
error: { code: 'FEATURE_ERROR', message: error.message },
fromCache: false
};
}
}
```
- **Tool Registration**:
- ✅ DO: Create tool definitions in `mcp-server/src/tools/`
- ✅ DO: Use Zod for parameter validation
- ✅ DO: Include optional tag parameter for multi-context support
- ✅ DO: Follow established naming conventions
```javascript
// ✅ DO: Register MCP tools with tagged system support
export function registerNewFeatureTool(server) {
server.addTool({
name: "new_feature",
description: "Description of the new feature with tagged task lists support",
inputSchema: z.object({
param: z.string().describe("Feature-specific parameter"),
tag: z.string().optional().describe("Target tag context (defaults to current tag)"),
force: z.boolean().optional().describe("Force operation without confirmation"),
projectRoot: z.string().optional().describe("Project root directory")
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
const result = await newFeatureDirect(
{ ...args, projectRoot: args.projectRoot },
log,
{ session }
);
return handleApiResult(result, log);
} catch (error) {
return handleApiResult({
success: false,
error: { code: 'EXECUTION_ERROR', message: error.message }
}, log);
}
})
});
}
```
## Testing Strategy
- **Unit Tests**:
- ✅ DO: Test core logic independently with both data formats
- ✅ DO: Mock file system operations appropriately
- ✅ DO: Test tag resolution behavior
- ✅ DO: Verify migration compatibility
```javascript
// ✅ DO: Test new features with tagged system awareness
describe('newFeature', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should work with legacy task format', async () => {
const legacyData = { tasks: [/* test data */] };
fs.readFileSync.mockReturnValue(JSON.stringify(legacyData));
const result = await newFeatureCore('/test/tasks.json', { param: 'test' });
expect(result).toBeDefined();
// Test legacy format handling
});
it('should work with tagged task format', async () => {
const taggedData = {
master: { tasks: [/* test data */] },
feature: { tasks: [/* test data */] }
};
fs.readFileSync.mockReturnValue(JSON.stringify(taggedData));
const result = await newFeatureCore('/test/tasks.json', { param: 'test' });
expect(result).toBeDefined();
// Test tagged format handling
});
it('should handle tag migration during feature usage', async () => {
const legacyData = { tasks: [/* test data */] };
fs.readFileSync.mockReturnValue(JSON.stringify(legacyData));
await newFeatureCore('/test/tasks.json', { param: 'test' });
// Verify migration occurred
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/test/tasks.json',
expect.stringContaining('"master"')
);
});
});
```
- **Integration Tests**:
- ✅ DO: Test CLI and MCP interfaces with real task data
- ✅ DO: Verify end-to-end workflows across tag contexts
- ✅ DO: Test error scenarios and recovery
## Documentation Updates
- **Rule Updates**:
- ✅ DO: Update relevant `.cursor/rules/*.mdc` files
- ✅ DO: Include tagged system considerations in architecture docs
- ✅ DO: Add examples showing multi-context usage
- ✅ DO: Update workflow documentation as needed
- **User Documentation**:
- ✅ DO: Add feature documentation to `/docs` folder
- ✅ DO: Include tagged system usage examples
- ✅ DO: Update command reference documentation
- ✅ DO: Provide migration notes if relevant
## Migration Considerations
- **Silent Migration Support**:
- ✅ DO: Ensure new features trigger migration when needed
- ✅ DO: Handle migration errors gracefully in feature code
- ✅ DO: Test feature behavior with pre-migration projects
- ❌ DON'T: Assume projects are already migrated
- **Tag Context Handling**:
- ✅ DO: Default to current tag when not specified
- ✅ DO: Support explicit tag selection in advanced features
- ✅ DO: Validate tag existence before operations
- ✅ DO: Provide clear messaging about tag context
## Performance Considerations
- **Efficient Tag Operations**:
- ✅ DO: Minimize file I/O operations per feature execution
- ✅ DO: Cache tag resolution results when appropriate
- ✅ DO: Use streaming for large task datasets
- ❌ DON'T: Load all tags when only one is needed
- **Memory Management**:
- ✅ DO: Process large task lists efficiently
- ✅ DO: Clean up temporary data structures
- ✅ DO: Avoid keeping all tag data in memory simultaneously
## Deployment and Versioning
- **Changesets**:
- ✅ DO: Create appropriate changesets for new features
- ✅ DO: Use semantic versioning (minor for new features)
- ✅ DO: Include tagged system information in release notes
- ✅ DO: Document breaking changes if any
- **Feature Flags**:
- ✅ DO: Consider feature flags for experimental functionality
- ✅ DO: Ensure tagged system features work with flags
- ✅ DO: Provide clear documentation about flag usage
By following these guidelines, new features will integrate smoothly with the Task Master ecosystem while supporting the enhanced tagged task lists system for multi-context development workflows.

View File

@@ -11,6 +11,8 @@ This document provides a detailed reference for interacting with Taskmaster, cov
**Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`. **Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`.
**Tagged Task Lists System:** Task Master now supports tagged task lists for multi-context task management. Existing projects automatically migrate to use a "master" tag with zero disruption. All commands continue to work unchanged while providing the foundation for enhanced multi-context workflows. MCP tools will be updated for full tagged support in Part 2 of the implementation.
--- ---
## Initialization & Setup ## Initialization & Setup

View File

@@ -3,9 +3,19 @@ description: Guidelines for implementing task management operations
globs: scripts/modules/task-manager.js globs: scripts/modules/task-manager.js
alwaysApply: false alwaysApply: false
--- ---
# Task Management Guidelines # Task Management Guidelines
## Tagged Task Lists System
Task Master now uses a **tagged task lists system** for multi-context task management:
- **Data Structure**: Tasks are organized into separate contexts (tags) within `tasks.json`
- **Legacy Format**: `{"tasks": [...]}`
- **Tagged Format**: `{"master": {"tasks": [...]}, "feature-branch": {"tasks": [...]}}`
- **Silent Migration**: Legacy format automatically converts to tagged format on first use
- **Tag Resolution**: Core functions receive legacy format for 100% backward compatibility
- **Default Tag**: "master" is used for all existing and new tasks unless otherwise specified
## Task Structure Standards ## Task Structure Standards
- **Core Task Properties**: - **Core Task Properties**:
@@ -28,6 +38,25 @@ alwaysApply: false
}; };
``` ```
- **Tagged Data Structure**:
- ✅ DO: Access tasks through tag resolution layer
- ✅ DO: Use `getTasksForTag(data, tagName)` to retrieve tasks for a specific tag
- ✅ DO: Use `setTasksForTag(data, tagName, tasks)` to update tasks for a specific tag
- ❌ DON'T: Directly manipulate the tagged structure in core functions
```javascript
// ✅ DO: Use tag resolution functions
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
// Manipulate tasks as normal...
// Save back to the tagged structure
setTasksForTag(tasksData, currentTag, tasks);
writeJSON(tasksPath, tasksData);
```
- **Subtask Structure**: - **Subtask Structure**:
- ✅ DO: Use consistent properties across subtasks - ✅ DO: Use consistent properties across subtasks
- ✅ DO: Maintain simple numeric IDs within parent tasks - ✅ DO: Maintain simple numeric IDs within parent tasks
@@ -48,53 +77,56 @@ alwaysApply: false
## Task Creation and Parsing ## Task Creation and Parsing
- **ID Management**: - **ID Management**:
- ✅ DO: Assign unique sequential IDs to tasks - ✅ DO: Assign unique sequential IDs to tasks within each tag context
- ✅ DO: Calculate the next ID based on existing tasks - ✅ DO: Calculate the next ID based on existing tasks in the current tag
- ❌ DON'T: Hardcode or reuse IDs - ❌ DON'T: Hardcode or reuse IDs within the same tag
```javascript ```javascript
// ✅ DO: Calculate the next available ID // ✅ DO: Calculate the next available ID within the current tag
const highestId = Math.max(...data.tasks.map(t => t.id)); const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
const highestId = Math.max(...tasks.map(t => t.id));
const nextTaskId = highestId + 1; const nextTaskId = highestId + 1;
``` ```
- **PRD Parsing**: - **PRD Parsing**:
- ✅ DO: Extract tasks from PRD documents using AI - ✅ DO: Extract tasks from PRD documents using AI
- ✅ DO: Create tasks in the current tag context (defaults to "master")
- ✅ DO: Provide clear prompts to guide AI task generation - ✅ DO: Provide clear prompts to guide AI task generation
- ✅ DO: Validate and clean up AI-generated tasks - ✅ DO: Validate and clean up AI-generated tasks
```javascript ```javascript
// ✅ DO: Validate AI responses // ✅ DO: Parse into current tag context
try { const tasksData = readJSON(tasksPath) || {};
// Parse the JSON response const currentTag = getCurrentTag() || 'master';
taskData = JSON.parse(jsonContent);
// Parse tasks and add to current tag
// Check that we have the required fields const newTasks = await parseTasksFromPRD(prdContent);
if (!taskData.title || !taskData.description) { setTasksForTag(tasksData, currentTag, newTasks);
throw new Error("Missing required fields in the generated task"); writeJSON(tasksPath, tasksData);
}
} catch (error) {
log('error', "Failed to parse AI's response as valid task JSON:", error);
process.exit(1);
}
``` ```
## Task Updates and Modifications ## Task Updates and Modifications
- **Status Management**: - **Status Management**:
- ✅ DO: Provide functions for updating task status - ✅ DO: Provide functions for updating task status within current tag context
- ✅ DO: Handle both individual tasks and subtasks - ✅ DO: Handle both individual tasks and subtasks
- ✅ DO: Consider subtask status when updating parent tasks - ✅ DO: Consider subtask status when updating parent tasks
```javascript ```javascript
// ✅ DO: Handle status updates for both tasks and subtasks // ✅ DO: Handle status updates within tagged context
async function setTaskStatus(tasksPath, taskIdInput, newStatus) { async function setTaskStatus(tasksPath, taskIdInput, newStatus) {
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
// Check if it's a subtask (e.g., "1.2") // Check if it's a subtask (e.g., "1.2")
if (taskIdInput.includes('.')) { if (taskIdInput.includes('.')) {
const [parentId, subtaskId] = taskIdInput.split('.').map(id => parseInt(id, 10)); const [parentId, subtaskId] = taskIdInput.split('.').map(id => parseInt(id, 10));
// Find the parent task and subtask // Find the parent task and subtask
const parentTask = data.tasks.find(t => t.id === parentId); const parentTask = tasks.find(t => t.id === parentId);
const subtask = parentTask.subtasks.find(st => st.id === subtaskId); const subtask = parentTask.subtasks.find(st => st.id === subtaskId);
// Update subtask status // Update subtask status
@@ -109,7 +141,7 @@ alwaysApply: false
} }
} else { } else {
// Handle regular task // Handle regular task
const task = data.tasks.find(t => t.id === parseInt(taskIdInput, 10)); const task = tasks.find(t => t.id === parseInt(taskIdInput, 10));
task.status = newStatus; task.status = newStatus;
// If marking as done, also mark subtasks // If marking as done, also mark subtasks
@@ -119,16 +151,24 @@ alwaysApply: false
}); });
} }
} }
// Save updated tasks back to tagged structure
setTasksForTag(tasksData, currentTag, tasks);
writeJSON(tasksPath, tasksData);
} }
``` ```
- **Task Expansion**: - **Task Expansion**:
- ✅ DO: Use AI to generate detailed subtasks - ✅ DO: Use AI to generate detailed subtasks within current tag context
- ✅ DO: Consider complexity analysis for subtask counts - ✅ DO: Consider complexity analysis for subtask counts
- ✅ DO: Ensure proper IDs for newly created subtasks - ✅ DO: Ensure proper IDs for newly created subtasks
```javascript ```javascript
// ✅ DO: Generate appropriate subtasks based on complexity // ✅ DO: Generate appropriate subtasks based on complexity
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
if (taskAnalysis) { if (taskAnalysis) {
log('info', `Found complexity analysis for task ${taskId}: Score ${taskAnalysis.complexityScore}/10`); log('info', `Found complexity analysis for task ${taskId}: Score ${taskAnalysis.complexityScore}/10`);
@@ -138,6 +178,11 @@ alwaysApply: false
log('info', `Using recommended number of subtasks: ${numSubtasks}`); log('info', `Using recommended number of subtasks: ${numSubtasks}`);
} }
} }
// Generate subtasks and save back
// ... subtask generation logic ...
setTasksForTag(tasksData, currentTag, tasks);
writeJSON(tasksPath, tasksData);
``` ```
## Task File Generation ## Task File Generation
@@ -155,67 +200,65 @@ alwaysApply: false
// Format dependencies with their status // Format dependencies with their status
if (task.dependencies && task.dependencies.length > 0) { if (task.dependencies && task.dependencies.length > 0) {
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, data.tasks)}\n`; content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, tasks)}\n`;
} else { } else {
content += '# Dependencies: None\n'; content += '# Dependencies: None\n';
} }
``` ```
- **Subtask Inclusion**: - **Tagged Context Awareness**:
- ✅ DO: Include subtasks in parent task files - ✅ DO: Generate task files from current tag context
- ✅ DO: Use consistent indentation for subtask sections - ✅ DO: Include tag information in generated files
- DO: Display subtask dependencies with proper formatting - DON'T: Mix tasks from different tags in file generation
```javascript ```javascript
// ✅ DO: Format subtasks correctly in task files // ✅ DO: Generate files for current tag context
if (task.subtasks && task.subtasks.length > 0) { async function generateTaskFiles(tasksPath, outputDir) {
content += '\n# Subtasks:\n'; const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
task.subtasks.forEach(subtask => { // Add tag context to file header
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`; let content = `# Tag Context: ${currentTag}\n`;
content += `# Task ID: ${task.id}\n`;
// Format subtask dependencies // ... rest of file generation
if (subtask.dependencies && subtask.dependencies.length > 0) {
// Format the dependencies
content += `### Dependencies: ${formattedDeps}\n`;
} else {
content += '### Dependencies: None\n';
}
content += `### Description: ${subtask.description || ''}\n`;
content += '### Details:\n';
content += (subtask.details || '').split('\n').map(line => line).join('\n');
content += '\n\n';
});
} }
``` ```
## Task Listing and Display ## Task Listing and Display
- **Filtering and Organization**: - **Filtering and Organization**:
- ✅ DO: Allow filtering tasks by status - ✅ DO: Allow filtering tasks by status within current tag context
- ✅ DO: Handle subtask display in lists - ✅ DO: Handle subtask display in lists
- ✅ DO: Use consistent table formats - ✅ DO: Use consistent table formats
```javascript ```javascript
// ✅ DO: Implement clear filtering and organization // ✅ DO: Implement clear filtering within tag context
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
// Filter tasks by status if specified // Filter tasks by status if specified
const filteredTasks = statusFilter const filteredTasks = statusFilter
? data.tasks.filter(task => ? tasks.filter(task =>
task.status && task.status.toLowerCase() === statusFilter.toLowerCase()) task.status && task.status.toLowerCase() === statusFilter.toLowerCase())
: data.tasks; : tasks;
``` ```
- **Progress Tracking**: - **Progress Tracking**:
- ✅ DO: Calculate and display completion statistics - ✅ DO: Calculate and display completion statistics for current tag
- ✅ DO: Track both task and subtask completion - ✅ DO: Track both task and subtask completion
- ✅ DO: Use visual progress indicators - ✅ DO: Use visual progress indicators
```javascript ```javascript
// ✅ DO: Track and display progress // ✅ DO: Track and display progress within tag context
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
// Calculate completion statistics // Calculate completion statistics
const totalTasks = data.tasks.length; const totalTasks = tasks.length;
const completedTasks = data.tasks.filter(task => const completedTasks = tasks.filter(task =>
task.status === 'done' || task.status === 'completed').length; task.status === 'done' || task.status === 'completed').length;
const completionPercentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; const completionPercentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
@@ -223,7 +266,7 @@ alwaysApply: false
let totalSubtasks = 0; let totalSubtasks = 0;
let completedSubtasks = 0; let completedSubtasks = 0;
data.tasks.forEach(task => { tasks.forEach(task => {
if (task.subtasks && task.subtasks.length > 0) { if (task.subtasks && task.subtasks.length > 0) {
totalSubtasks += task.subtasks.length; totalSubtasks += task.subtasks.length;
completedSubtasks += task.subtasks.filter(st => completedSubtasks += task.subtasks.filter(st =>
@@ -232,99 +275,52 @@ alwaysApply: false
}); });
``` ```
## Complexity Analysis ## Migration and Compatibility
- **Scoring System**: - **Silent Migration Handling**:
- ✅ DO: Use AI to analyze task complexity - ✅ DO: Implement silent migration in `readJSON()` function
- ✅ DO: Include complexity scores (1-10) - ✅ DO: Detect legacy format and convert automatically
- ✅ DO: Generate specific expansion recommendations - ✅ DO: Preserve all existing task data during migration
```javascript ```javascript
// ✅ DO: Handle complexity analysis properly // ✅ DO: Handle silent migration (implemented in utils.js)
const report = { function readJSON(filepath) {
meta: { let data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
generatedAt: new Date().toISOString(),
tasksAnalyzed: tasksData.tasks.length,
thresholdScore: thresholdScore,
projectName: tasksData.meta?.projectName || 'Your Project Name',
usedResearch: useResearch
},
complexityAnalysis: complexityAnalysis
};
```
- **Analysis-Based Workflow**:
- ✅ DO: Use complexity reports to guide task expansion
- ✅ DO: Prioritize complex tasks for more detailed breakdown
- ✅ DO: Use expansion prompts from complexity analysis
```javascript
// ✅ DO: Apply complexity analysis to workflow
// Sort tasks by complexity if report exists, otherwise by ID
if (complexityReport && complexityReport.complexityAnalysis) {
log('info', 'Sorting tasks by complexity...');
// Create a map of task IDs to complexity scores // Silent migration for tasks.json files
const complexityMap = new Map(); if (data.tasks && Array.isArray(data.tasks) && !data.master && isTasksFile) {
complexityReport.complexityAnalysis.forEach(analysis => { const migratedData = {
complexityMap.set(analysis.taskId, analysis.complexityScore); master: {
}); tasks: data.tasks
}
};
writeJSON(filepath, migratedData);
data = migratedData;
}
// Sort tasks by complexity score (high to low) return data;
tasksToExpand.sort((a, b) => {
const scoreA = complexityMap.get(a.id) || 0;
const scoreB = complexityMap.get(b.id) || 0;
return scoreB - scoreA;
});
} }
``` ```
## Next Task Selection - **Tag Resolution**:
- ✅ DO: Use tag resolution functions to maintain backward compatibility
- **Eligibility Criteria**: - ✅ DO: Return legacy format to core functions
- DO: Consider dependencies when finding next tasks - DON'T: Expose tagged structure to existing core logic
- ✅ DO: Prioritize by task priority and dependency count
- ✅ DO: Skip completed tasks
```javascript ```javascript
// ✅ DO: Use proper task prioritization logic // ✅ DO: Use tag resolution layer
function findNextTask(tasks) { function getTasksForTag(data, tagName) {
// Get all completed task IDs if (data.tasks && Array.isArray(data.tasks)) {
const completedTaskIds = new Set( // Legacy format - return as-is
tasks return data.tasks;
.filter(t => t.status === 'done' || t.status === 'completed') }
.map(t => t.id)
);
// Filter for pending tasks whose dependencies are all satisfied if (data[tagName] && data[tagName].tasks) {
const eligibleTasks = tasks.filter(task => // Tagged format - return tasks for specified tag
(task.status === 'pending' || task.status === 'in-progress') && return data[tagName].tasks;
task.dependencies && }
task.dependencies.every(depId => completedTaskIds.has(depId))
);
// Sort by priority, dependency count, and ID return [];
const priorityValues = { 'high': 3, 'medium': 2, 'low': 1 };
const nextTask = eligibleTasks.sort((a, b) => {
// Priority first
const priorityA = priorityValues[a.priority || 'medium'] || 2;
const priorityB = priorityValues[b.priority || 'medium'] || 2;
if (priorityB !== priorityA) {
return priorityB - priorityA; // Higher priority first
}
// Dependency count next
if (a.dependencies.length !== b.dependencies.length) {
return a.dependencies.length - b.dependencies.length; // Fewer dependencies first
}
// ID last
return a.id - b.id; // Lower ID first
})[0];
return nextTask;
} }
``` ```

View File

@@ -1,6 +1,6 @@
--- ---
description: Guidelines for implementing utility functions description:
globs: scripts/modules/utils.js, mcp-server/src/**/* globs:
alwaysApply: false alwaysApply: false
--- ---
# Utility Function Guidelines # Utility Function Guidelines
@@ -600,4 +600,578 @@ export {
- ✅ DO: Sort discovered task IDs numerically for better readability - ✅ DO: Sort discovered task IDs numerically for better readability
- ❌ DON'T: Replace explicit user task selections with fuzzy results - ❌ DON'T: Replace explicit user task selections with fuzzy results
Refer to [`context_gathering.mdc`](mdc:.cursor/rules/context_gathering.mdc) for detailed implementation patterns, [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) and [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc) for more context on MCP server architecture and integration. Refer to [`context_gathering.mdc`](mdc:.cursor/rules/context_gathering.mdc) for detailed implementation patterns, [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) and [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc) for more context on MCP server architecture and integration.
## File System Operations
- **JSON File Handling**:
- ✅ DO: Use `readJSON` and `writeJSON` for all JSON operations
- ✅ DO: Include error handling for file operations
- ✅ DO: Validate JSON structure after reading
- ❌ DON'T: Use raw `fs.readFileSync` or `fs.writeFileSync` for JSON
```javascript
// ✅ DO: Use utility functions with error handling
function readJSON(filepath) {
try {
if (!fs.existsSync(filepath)) {
return null; // or appropriate default
}
let data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
// Silent migration for tasks.json files: Transform old format to tagged format
const isTasksFile = filepath.includes('tasks.json') || path.basename(filepath) === 'tasks.json';
if (data && data.tasks && Array.isArray(data.tasks) && !data.master && isTasksFile) {
// Migrate from old format { "tasks": [...] } to new format { "master": { "tasks": [...] } }
const migratedData = {
master: {
tasks: data.tasks
}
};
writeJSON(filepath, migratedData);
// Set global flag for CLI notice and perform complete migration
global.taskMasterMigrationOccurred = true;
performCompleteTagMigration(filepath);
data = migratedData;
}
return data;
} catch (error) {
log('error', `Failed to read JSON from ${filepath}: ${error.message}`);
return null;
}
}
function writeJSON(filepath, data) {
try {
const dirPath = path.dirname(filepath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
} catch (error) {
log('error', `Failed to write JSON to ${filepath}: ${error.message}`);
throw error;
}
}
```
- **Path Resolution**:
- ✅ DO: Use `path.join()` for cross-platform path construction
- ✅ DO: Use `path.resolve()` for absolute paths
- ✅ DO: Validate paths before file operations
```javascript
// ✅ DO: Handle paths correctly
function findProjectRoot(startPath = process.cwd()) {
let currentPath = path.resolve(startPath);
const rootPath = path.parse(currentPath).root;
while (currentPath !== rootPath) {
const taskMasterPath = path.join(currentPath, '.taskmaster');
if (fs.existsSync(taskMasterPath)) {
return currentPath;
}
currentPath = path.dirname(currentPath);
}
return null; // Not found
}
```
## Tagged Task Lists System Utilities
- **Tag Resolution Functions**:
- ✅ DO: Use tag resolution layer for all task data access
- ✅ DO: Provide backward compatibility with legacy format
- ✅ DO: Default to "master" tag when no tag is specified
```javascript
// ✅ DO: Implement tag resolution functions
function getTasksForTag(data, tagName = 'master') {
if (!data) {
return [];
}
// Handle legacy format - direct tasks array
if (data.tasks && Array.isArray(data.tasks)) {
return data.tasks;
}
// Handle tagged format - tasks under specific tag
if (data[tagName] && data[tagName].tasks && Array.isArray(data[tagName].tasks)) {
return data[tagName].tasks;
}
return [];
}
function setTasksForTag(data, tagName = 'master', tasks) {
// Ensure data object exists
if (!data) {
data = {};
}
// Create tag structure if it doesn't exist
if (!data[tagName]) {
data[tagName] = {};
}
// Set tasks for the tag
data[tagName].tasks = tasks;
return data;
}
function getCurrentTag() {
// Get current tag from state.json or default to 'master'
try {
const projectRoot = findProjectRoot();
if (!projectRoot) return 'master';
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(statePath)) {
const state = readJSON(statePath);
return state.currentTag || 'master';
}
} catch (error) {
log('debug', `Error reading current tag: ${error.message}`);
}
return 'master';
}
```
- **Migration Functions**:
- ✅ DO: Implement complete migration for all related files
- ✅ DO: Handle configuration and state file creation
- ✅ DO: Provide migration status tracking
```javascript
// ✅ DO: Implement complete migration system
function performCompleteTagMigration(tasksJsonPath) {
try {
// Derive project root from tasks.json path
const projectRoot = findProjectRoot(path.dirname(tasksJsonPath)) || path.dirname(tasksJsonPath);
// 1. Migrate config.json - add defaultTag and tags section
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
migrateConfigJson(configPath);
}
// 2. Create state.json if it doesn't exist
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
if (getDebugFlag()) {
log('debug', 'Completed tagged task lists migration for project');
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error during complete tag migration: ${error.message}`);
}
}
}
function migrateConfigJson(configPath) {
try {
const config = readJSON(configPath);
if (!config) return;
let modified = false;
// Add global.defaultTag if missing
if (!config.global) {
config.global = {};
}
if (!config.global.defaultTag) {
config.global.defaultTag = 'master';
modified = true;
}
// Add tags section if missing
if (!config.tags) {
config.tags = {
enabledGitworkflow: false,
autoSwitchTagWithBranch: false
};
modified = true;
}
if (modified) {
writeJSON(configPath, config);
if (getDebugFlag()) {
log('debug', 'Updated config.json with tagged task system settings');
}
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error migrating config.json: ${error.message}`);
}
}
}
function createStateJson(statePath) {
try {
const initialState = {
currentTag: 'master',
lastSwitched: new Date().toISOString(),
branchTagMapping: {},
migrationNoticeShown: false
};
writeJSON(statePath, initialState);
if (getDebugFlag()) {
log('debug', 'Created initial state.json for tagged task system');
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error creating state.json: ${error.message}`);
}
}
}
function markMigrationForNotice() {
try {
const projectRoot = findProjectRoot();
if (!projectRoot) return;
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
const state = readJSON(statePath) || {};
state.migrationNoticeShown = false; // Reset to show notice
writeJSON(statePath, state);
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error marking migration for notice: ${error.message}`);
}
}
}
```
## Logging Functions
- **Consistent Logging**:
- ✅ DO: Use the central `log` function for all output
- ✅ DO: Use appropriate log levels (info, warn, error, debug)
- ✅ DO: Support silent mode for programmatic usage
```javascript
// ✅ DO: Implement consistent logging with silent mode
let silentMode = false;
function log(level, ...messages) {
if (silentMode && level !== 'error') {
return; // Suppress non-error logs in silent mode
}
const timestamp = new Date().toISOString();
const formattedMessage = messages.join(' ');
switch (level) {
case 'error':
console.error(`[ERROR] ${formattedMessage}`);
break;
case 'warn':
console.warn(`[WARN] ${formattedMessage}`);
break;
case 'info':
console.log(`[INFO] ${formattedMessage}`);
break;
case 'debug':
if (getDebugFlag()) {
console.log(`[DEBUG] ${formattedMessage}`);
}
break;
default:
console.log(formattedMessage);
}
}
function enableSilentMode() {
silentMode = true;
}
function disableSilentMode() {
silentMode = false;
}
function isSilentMode() {
return silentMode;
}
```
## Task Utilities
- **Task Finding and Manipulation**:
- ✅ DO: Use tagged task system aware functions
- ✅ DO: Handle both task and subtask operations
- ✅ DO: Validate task IDs before operations
```javascript
// ✅ DO: Implement tag-aware task utilities
function findTaskById(tasks, taskId) {
if (!Array.isArray(tasks)) {
return null;
}
return tasks.find(task => task.id === taskId) || null;
}
function findSubtaskById(tasks, parentId, subtaskId) {
const parentTask = findTaskById(tasks, parentId);
if (!parentTask || !parentTask.subtasks) {
return null;
}
return parentTask.subtasks.find(subtask => subtask.id === subtaskId) || null;
}
function getNextTaskId(tasks) {
if (!Array.isArray(tasks) || tasks.length === 0) {
return 1;
}
const maxId = Math.max(...tasks.map(task => task.id));
return maxId + 1;
}
function getNextSubtaskId(parentTask) {
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
return 1;
}
const maxId = Math.max(...parentTask.subtasks.map(subtask => subtask.id));
return maxId + 1;
}
```
## String Utilities
- **Text Processing**:
- ✅ DO: Handle text truncation appropriately
- ✅ DO: Provide consistent formatting functions
- ✅ DO: Support different output formats
```javascript
// ✅ DO: Implement useful string utilities
function truncate(str, maxLength = 50) {
if (!str || typeof str !== 'string') {
return '';
}
if (str.length <= maxLength) {
return str;
}
return str.substring(0, maxLength - 3) + '...';
}
function formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
function capitalizeFirst(str) {
if (!str || typeof str !== 'string') {
return '';
}
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
```
## Dependency Management Utilities
- **Dependency Analysis**:
- ✅ DO: Detect circular dependencies
- ✅ DO: Validate dependency references
- ✅ DO: Support cross-tag dependency checking (future enhancement)
```javascript
// ✅ DO: Implement dependency utilities
function findCycles(tasks) {
const cycles = [];
const visited = new Set();
const recStack = new Set();
function dfs(taskId, path = []) {
if (recStack.has(taskId)) {
// Found a cycle
const cycleStart = path.indexOf(taskId);
const cycle = path.slice(cycleStart).concat([taskId]);
cycles.push(cycle);
return;
}
if (visited.has(taskId)) {
return;
}
visited.add(taskId);
recStack.add(taskId);
const task = findTaskById(tasks, taskId);
if (task && task.dependencies) {
task.dependencies.forEach(depId => {
dfs(depId, path.concat([taskId]));
});
}
recStack.delete(taskId);
}
tasks.forEach(task => {
if (!visited.has(task.id)) {
dfs(task.id);
}
});
return cycles;
}
function validateDependencies(tasks) {
const validationErrors = [];
const taskIds = new Set(tasks.map(task => task.id));
tasks.forEach(task => {
if (task.dependencies) {
task.dependencies.forEach(depId => {
if (!taskIds.has(depId)) {
validationErrors.push({
taskId: task.id,
invalidDependency: depId,
message: `Task ${task.id} depends on non-existent task ${depId}`
});
}
});
}
});
return validationErrors;
}
```
## Environment and Configuration Utilities
- **Environment Variable Resolution**:
- ✅ DO: Support both `.env` files and MCP session environment
- ✅ DO: Provide fallbacks for missing values
- ✅ DO: Handle API key resolution correctly
```javascript
// ✅ DO: Implement flexible environment resolution
function resolveEnvVariable(key, sessionEnv = null) {
// First check session environment (for MCP)
if (sessionEnv && sessionEnv[key]) {
return sessionEnv[key];
}
// Then check process environment
if (process.env[key]) {
return process.env[key];
}
// Finally try .env file if in project root
try {
const projectRoot = findProjectRoot();
if (projectRoot) {
const envPath = path.join(projectRoot, '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
const lines = envContent.split('\n');
for (const line of lines) {
const [envKey, envValue] = line.split('=');
if (envKey && envKey.trim() === key) {
return envValue ? envValue.trim().replace(/^["']|["']$/g, '') : undefined;
}
}
}
}
} catch (error) {
log('debug', `Error reading .env file: ${error.message}`);
}
return undefined;
}
function getDebugFlag() {
const debugFlag = resolveEnvVariable('TASKMASTER_DEBUG') ||
resolveEnvVariable('DEBUG') ||
'false';
return debugFlag.toLowerCase() === 'true';
}
```
## Export Pattern
- **Module Exports**:
- ✅ DO: Export all utility functions explicitly
- ✅ DO: Group related functions logically
- ✅ DO: Include new tagged system utilities
```javascript
// ✅ DO: Export utilities in logical groups
module.exports = {
// File system utilities
readJSON,
writeJSON,
findProjectRoot,
// Tagged task system utilities
getTasksForTag,
setTasksForTag,
getCurrentTag,
performCompleteTagMigration,
migrateConfigJson,
createStateJson,
markMigrationForNotice,
// Logging utilities
log,
enableSilentMode,
disableSilentMode,
isSilentMode,
// Task utilities
findTaskById,
findSubtaskById,
getNextTaskId,
getNextSubtaskId,
// String utilities
truncate,
formatDuration,
capitalizeFirst,
// Dependency utilities
findCycles,
validateDependencies,
// Environment utilities
resolveEnvVariable,
getDebugFlag,
// Legacy utilities (maintained for compatibility)
aggregateTelemetry
};
```
Refer to [`utils.js`](mdc:scripts/modules/utils.js) for implementation examples and [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc) for integration patterns.

View File

@@ -32,10 +32,7 @@
"defaultTag": "master" "defaultTag": "master"
}, },
"tags": { "tags": {
"autoSwitchOnBranch": false, "enabledGitworkflow": false,
"gitIntegration": { "autoSwitchTagWithBranch": false
"enabled": false,
"autoSwitchTagWithBranch": false
}
} }
} }

View File

@@ -219,7 +219,7 @@ Create a new core function (e.g., research.js) in scripts/modules/task-manager/
The research command now provides the same polished user experience as other AI-powered commands in the system. The research command now provides the same polished user experience as other AI-powered commands in the system.
</info added on 2025-05-25T06:29:01.194Z> </info added on 2025-05-25T06:29:01.194Z>
## 5. Direct Function Implementation [pending] ## 5. Direct Function Implementation [done]
### Dependencies: 98.4 ### Dependencies: 98.4
### Description: Create the MCP direct function wrapper in mcp-server/src/core/direct-functions/ following the add-task pattern ### Description: Create the MCP direct function wrapper in mcp-server/src/core/direct-functions/ following the add-task pattern
### Details: ### Details:
@@ -233,7 +233,7 @@ Create a new direct function (e.g., research.js) in mcp-server/src/core/direct-f
- Handles telemetry data propagation - Handles telemetry data propagation
- Export and register in task-master-core.js - Export and register in task-master-core.js
## 6. MCP Tool Implementation [pending] ## 6. MCP Tool Implementation [done]
### Dependencies: 98.5 ### Dependencies: 98.5
### Description: Create the MCP tool in mcp-server/src/tools/ following the add-task tool pattern ### Description: Create the MCP tool in mcp-server/src/tools/ following the add-task tool pattern
### Details: ### Details:

View File

@@ -115,7 +115,7 @@ Ensure all features work as intended and meet quality standards, with specific f
### Details: ### Details:
## 14. Create State Management Utilities [pending] ## 14. Create State Management Utilities [done]
### Dependencies: 103.3, 103.7 ### Dependencies: 103.3, 103.7
### Description: Implement utilities for reading/writing current tag state, tag resolution logic (currentTag from state -> --tag flag -> defaultTag fallback), and state file validation ### Description: Implement utilities for reading/writing current tag state, tag resolution logic (currentTag from state -> --tag flag -> defaultTag fallback), and state file validation
### Details: ### Details:
@@ -145,3 +145,9 @@ COMPLETED: All documentation files have been successfully updated to reflect the
All documentation now properly reflects Part 1 implementation and prepares for Part 2 features. Documentation is fully aligned with the new tagged task structure. All documentation now properly reflects Part 1 implementation and prepares for Part 2 features. Documentation is fully aligned with the new tagged task structure.
</info added on 2025-06-11T21:12:52.662Z> </info added on 2025-06-11T21:12:52.662Z>
## 17. Implement Task Template Importing from External .json Files [pending]
### Dependencies: None
### Description: Implement a mechanism to import tasks from external .json files, treating them as task templates. This allows users to add new .json files to the .taskmaster/tasks folder. The system should read these files, extract tasks under a specific tag, and merge them into the main tasks.json. The 'master' tag from template files must be ignored to prevent conflicts, and the primary tasks.json file will always take precedence over imported tags.
### Details:
Key implementation steps: 1. Develop a file watcher or a manual import command to detect and process new .json files in the tasks directory. 2. Implement logic to read an external json file, identify the tag key, and extract the array of tasks. 3. Handle potential conflicts: if an imported tag already exists in the main tasks.json, the existing tasks should be preserved and new ones appended, or the import should be skipped based on a defined precedence rule. 4. Ignore any 'master' key in template files to protect the integrity of the main task list. 5. Update task ID sequencing to ensure imported tasks are assigned unique IDs that don't conflict with existing tasks.

File diff suppressed because one or more lines are too long

View File

@@ -38,11 +38,16 @@ Taskmaster uses two primary methods for configuration:
"debug": false, "debug": false,
"defaultSubtasks": 5, "defaultSubtasks": 5,
"defaultPriority": "medium", "defaultPriority": "medium",
"defaultTag": "master",
"projectName": "Your Project Name", "projectName": "Your Project Name",
"ollamaBaseURL": "http://localhost:11434/api", "ollamaBaseURL": "http://localhost:11434/api",
"azureBaseURL": "https://your-endpoint.azure.com/", "azureBaseURL": "https://your-endpoint.azure.com/",
"vertexProjectId": "your-gcp-project-id", "vertexProjectId": "your-gcp-project-id",
"vertexLocation": "us-central1" "vertexLocation": "us-central1"
},
"tags": {
"enabledGitworkflow": false,
"autoSwitchTagWithBranch": false
} }
} }
``` ```
@@ -79,6 +84,52 @@ Taskmaster uses two primary methods for configuration:
**Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables. **Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables.
## Tagged Task Lists Configuration (v0.17+)
Taskmaster includes a tagged task lists system for multi-context task management. The following settings control this functionality:
### Tags Configuration Section
```json
"tags": {
"enabledGitworkflow": false,
"autoSwitchTagWithBranch": false
}
```
- **`enabledGitworkflow`** (boolean): Enable git branch integration features (Part 2 feature)
- **`autoSwitchTagWithBranch`** (boolean): Automatically switch tag context when git branch changes
### Global Tag Settings
```json
"global": {
"defaultTag": "master"
}
```
- **`defaultTag`** (string): Default tag context for new operations (default: "master")
## State Management File
Taskmaster uses `.taskmaster/state.json` to track tagged system runtime information:
```json
{
"currentTag": "master",
"lastSwitched": "2025-06-11T20:26:12.598Z",
"branchTagMapping": {},
"migrationNoticeShown": true
}
```
- **`currentTag`**: Currently active tag context
- **`lastSwitched`**: Timestamp of last tag switch
- **`branchTagMapping`**: Mapping between git branches and tag names
- **`migrationNoticeShown`**: Whether migration notice has been displayed
This file is automatically created during tagged system migration and should not be manually edited.
## Example `.env` File (for API Keys) ## Example `.env` File (for API Keys)
``` ```

View File

@@ -130,7 +130,8 @@ export function registerShowTaskTool(server) {
result, result,
log, log,
'Error retrieving task details', 'Error retrieving task details',
processTaskResponse processTaskResponse,
projectRoot
); );
} catch (error) { } catch (error) {
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`); log.error(`Error in get-task tool: ${error.message}\n${error.stack}`);

View File

@@ -91,7 +91,13 @@ export function registerListTasksTool(server) {
log.info( log.info(
`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks` `Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
); );
return handleApiResult(result, log, 'Error getting tasks'); return handleApiResult(
result,
log,
'Error getting tasks',
undefined,
args.projectRoot
);
} catch (error) { } catch (error) {
log.error(`Error getting tasks: ${error.message}`); log.error(`Error getting tasks: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);

View File

@@ -8,6 +8,7 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { contextManager } from '../core/context-manager.js'; // Import the singleton import { contextManager } from '../core/context-manager.js'; // Import the singleton
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { getCurrentTag } from '../../../scripts/modules/utils.js';
// Import path utilities to ensure consistent path resolution // Import path utilities to ensure consistent path resolution
import { import {
@@ -59,6 +60,64 @@ function getVersionInfo() {
} }
} }
/**
* Get current tag information for MCP responses
* @param {string} projectRoot - The project root directory
* @param {Object} log - Logger object
* @returns {Object} Tag information object
*/
function getTagInfo(projectRoot, log) {
try {
if (!projectRoot) {
log.warn('No project root provided for tag information');
return { currentTag: 'master', availableTags: ['master'] };
}
const currentTag = getCurrentTag(projectRoot);
// Read available tags from tasks.json
let availableTags = ['master']; // Default fallback
try {
const tasksJsonPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
if (fs.existsSync(tasksJsonPath)) {
const tasksData = JSON.parse(fs.readFileSync(tasksJsonPath, 'utf-8'));
// If it's the new tagged format, extract tag keys
if (
tasksData &&
typeof tasksData === 'object' &&
!Array.isArray(tasksData.tasks)
) {
const tagKeys = Object.keys(tasksData).filter(
(key) =>
tasksData[key] &&
typeof tasksData[key] === 'object' &&
Array.isArray(tasksData[key].tasks)
);
if (tagKeys.length > 0) {
availableTags = tagKeys;
}
}
}
} catch (tagError) {
log.debug(`Could not read available tags: ${tagError.message}`);
}
return {
currentTag: currentTag || 'master',
availableTags: availableTags
};
} catch (error) {
log.warn(`Error getting tag information: ${error.message}`);
return { currentTag: 'master', availableTags: ['master'] };
}
}
/** /**
* Get normalized project root path * Get normalized project root path
* @param {string|undefined} projectRootRaw - Raw project root from arguments * @param {string|undefined} projectRootRaw - Raw project root from arguments
@@ -242,21 +301,26 @@ function getProjectRootFromSession(session, log) {
* @param {Object} log - Logger object * @param {Object} log - Logger object
* @param {string} errorPrefix - Prefix for error messages * @param {string} errorPrefix - Prefix for error messages
* @param {Function} processFunction - Optional function to process successful result data * @param {Function} processFunction - Optional function to process successful result data
* @param {string} [projectRoot] - Optional project root for tag information
* @returns {Object} - Standardized MCP response object * @returns {Object} - Standardized MCP response object
*/ */
async function handleApiResult( async function handleApiResult(
result, result,
log, log,
errorPrefix = 'API error', errorPrefix = 'API error',
processFunction = processMCPResponseData processFunction = processMCPResponseData,
projectRoot = null
) { ) {
// Get version info for every response // Get version info for every response
const versionInfo = getVersionInfo(); const versionInfo = getVersionInfo();
// Get tag info if project root is provided
const tagInfo = projectRoot ? getTagInfo(projectRoot, log) : null;
if (!result.success) { if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`; const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
log.error(`${errorPrefix}: ${errorMsg}`); log.error(`${errorPrefix}: ${errorMsg}`);
return createErrorResponse(errorMsg, versionInfo); return createErrorResponse(errorMsg, versionInfo, tagInfo);
} }
// Process the result data if needed // Process the result data if needed
@@ -266,12 +330,17 @@ async function handleApiResult(
log.info('Successfully completed operation'); log.info('Successfully completed operation');
// Create the response payload including version info // Create the response payload including version info and tag info
const responsePayload = { const responsePayload = {
data: processedData, data: processedData,
version: versionInfo version: versionInfo
}; };
// Add tag information if available
if (tagInfo) {
responsePayload.tag = tagInfo;
}
return createContentResponse(responsePayload); return createContentResponse(responsePayload);
} }
@@ -496,21 +565,30 @@ function createContentResponse(content) {
* Creates error response for tools * Creates error response for tools
* @param {string} errorMessage - Error message to include in response * @param {string} errorMessage - Error message to include in response
* @param {Object} [versionInfo] - Optional version information object * @param {Object} [versionInfo] - Optional version information object
* @param {Object} [tagInfo] - Optional tag information object
* @returns {Object} - Error content response object in FastMCP format * @returns {Object} - Error content response object in FastMCP format
*/ */
function createErrorResponse(errorMessage, versionInfo) { function createErrorResponse(errorMessage, versionInfo, tagInfo) {
// Provide fallback version info if not provided // Provide fallback version info if not provided
if (!versionInfo) { if (!versionInfo) {
versionInfo = getVersionInfo(); versionInfo = getVersionInfo();
} }
let responseText = `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`;
// Add tag information if available
if (tagInfo) {
responseText += `
Current Tag: ${tagInfo.currentTag}`;
}
return { return {
content: [ content: [
{ {
type: 'text', type: 'text',
text: `Error: ${errorMessage} text: responseText
Version: ${versionInfo.version}
Name: ${versionInfo.name}`
} }
], ],
isError: true isError: true
@@ -704,6 +782,7 @@ function withNormalizedProjectRoot(executeFn) {
export { export {
getProjectRoot, getProjectRoot,
getProjectRootFromSession, getProjectRootFromSession,
getTagInfo,
handleApiResult, handleApiResult,
executeTaskMasterCommand, executeTaskMasterCommand,
getCachedOrExecute, getCachedOrExecute,

24358
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,12 @@ import {
getVertexProjectId, getVertexProjectId,
getVertexLocation getVertexLocation
} from './config-manager.js'; } from './config-manager.js';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js'; import {
log,
findProjectRoot,
resolveEnvVariable,
getCurrentTag
} from './utils.js';
// Import provider classes // Import provider classes
import { import {
@@ -86,6 +91,65 @@ function _getCostForModel(providerName, modelId) {
}; };
} }
// Helper function to get tag information for responses
function _getTagInfo(projectRoot) {
try {
if (!projectRoot) {
return { currentTag: 'master', availableTags: ['master'] };
}
const currentTag = getCurrentTag(projectRoot);
// Read available tags from tasks.json
let availableTags = ['master']; // Default fallback
try {
const path = require('path');
const fs = require('fs');
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
if (fs.existsSync(tasksPath)) {
const tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
if (tasksData && typeof tasksData === 'object') {
// Check if it's tagged format (has tag-like keys with tasks arrays)
const potentialTags = Object.keys(tasksData).filter(
(key) =>
tasksData[key] &&
typeof tasksData[key] === 'object' &&
Array.isArray(tasksData[key].tasks)
);
if (potentialTags.length > 0) {
availableTags = potentialTags;
}
}
}
} catch (readError) {
// Silently fall back to default if we can't read tasks file
if (getDebugFlag()) {
log(
'debug',
`Could not read tasks file for available tags: ${readError.message}`
);
}
}
return {
currentTag: currentTag || 'master',
availableTags: availableTags
};
} catch (error) {
if (getDebugFlag()) {
log('debug', `Error getting tag information: ${error.message}`);
}
return { currentTag: 'master', availableTags: ['master'] };
}
}
// --- Configuration for Retries --- // --- Configuration for Retries ---
const MAX_RETRIES = 2; const MAX_RETRIES = 2;
const INITIAL_RETRY_DELAY_MS = 1000; const INITIAL_RETRY_DELAY_MS = 1000;
@@ -246,7 +310,7 @@ async function _attemptProviderCallWithRetries(
if (isRetryableError(error) && retries < MAX_RETRIES) { if (isRetryableError(error) && retries < MAX_RETRIES) {
retries++; retries++;
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1); const delay = INITIAL_RETRY_DELAY_MS * 2 ** (retries - 1);
log( log(
'info', 'info',
`Something went wrong on the provider side. Retrying in ${delay / 1000}s...` `Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
@@ -327,14 +391,14 @@ async function _unifiedServiceRunner(serviceType, params) {
'AI service call failed for all configured roles.'; 'AI service call failed for all configured roles.';
for (const currentRole of sequence) { for (const currentRole of sequence) {
let providerName, let providerName;
modelId, let modelId;
apiKey, let apiKey;
roleParams, let roleParams;
provider, let provider;
baseURL, let baseURL;
providerResponse, let providerResponse;
telemetryData = null; let telemetryData = null;
try { try {
log('info', `New AI service call with role: ${currentRole}`); log('info', `New AI service call with role: ${currentRole}`);
@@ -555,9 +619,13 @@ async function _unifiedServiceRunner(serviceType, params) {
finalMainResult = providerResponse; finalMainResult = providerResponse;
} }
// Get tag information for the response
const tagInfo = _getTagInfo(effectiveProjectRoot);
return { return {
mainResult: finalMainResult, mainResult: finalMainResult,
telemetryData: telemetryData telemetryData: telemetryData,
tagInfo: tagInfo
}; };
} catch (error) { } catch (error) {
const cleanMessage = _extractErrorMessage(error); const cleanMessage = _extractErrorMessage(error);

View File

@@ -12,12 +12,14 @@ import {
stopLoadingIndicator, stopLoadingIndicator,
succeedLoadingIndicator, succeedLoadingIndicator,
failLoadingIndicator, failLoadingIndicator,
displayAiUsageSummary displayAiUsageSummary,
displayContextAnalysis
} from '../ui.js'; } from '../ui.js';
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js'; import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js'; import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js'; import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from './generate-task-files.js';
import ContextGatherer from '../utils/contextGatherer.js';
// Define Zod schema for the expected AI output object // Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({ const AiTaskDataSchema = z.object({
@@ -199,7 +201,9 @@ async function addTask(
const invalidDeps = dependencies.filter((depId) => { const invalidDeps = dependencies.filter((depId) => {
// Ensure depId is parsed as a number for comparison // Ensure depId is parsed as a number for comparison
const numDepId = parseInt(depId, 10); const numDepId = parseInt(depId, 10);
return isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId); return (
Number.isNaN(numDepId) || !data.tasks.some((t) => t.id === numDepId)
);
}); });
if (invalidDeps.length > 0) { if (invalidDeps.length > 0) {
@@ -262,561 +266,27 @@ async function addTask(
// --- Refactored AI Interaction --- // --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
// Create context string for task creation prompt // --- Use the new ContextGatherer ---
let contextTasks = ''; const contextGatherer = new ContextGatherer(projectRoot);
const gatherResult = await contextGatherer.gather({
// Create a dependency map for better understanding of the task relationships semanticQuery: prompt,
const taskMap = {}; dependencyTasks: numericDependencies,
data.tasks.forEach((t) => { format: 'research'
// For each task, only include id, title, description, and dependencies
taskMap[t.id] = {
id: t.id,
title: t.title,
description: t.description,
dependencies: t.dependencies || [],
status: t.status
};
}); });
// CLI-only feedback for the dependency analysis const gatheredContext = gatherResult.context;
if (outputFormat === 'text') { const analysisData = gatherResult.analysisData;
console.log(
boxen(chalk.cyan.bold('Task Context Analysis'), { // Display context analysis if not in silent mode
padding: { top: 0, bottom: 0, left: 1, right: 1 }, if (outputFormat === 'text' && analysisData) {
margin: { top: 0, bottom: 0 }, displayContextAnalysis(analysisData, prompt, gatheredContext.length);
borderColor: 'cyan',
borderStyle: 'round'
})
);
} }
// Initialize variables that will be used in either branch
let uniqueDetailedTasks = [];
let dependentTasks = [];
let promptCategory = null;
if (numericDependencies.length > 0) {
// If specific dependencies were provided, focus on them
// Get all tasks that were found in the dependency graph
dependentTasks = Array.from(allRelatedTaskIds)
.map((id) => data.tasks.find((t) => t.id === id))
.filter(Boolean);
// Sort by depth in the dependency chain
dependentTasks.sort((a, b) => {
const depthA = depthMap.get(a.id) || 0;
const depthB = depthMap.get(b.id) || 0;
return depthA - depthB; // Lowest depth (root dependencies) first
});
// Limit the number of detailed tasks to avoid context explosion
uniqueDetailedTasks = dependentTasks.slice(0, 8);
contextTasks = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.\n\nDirect dependencies:`;
const directDeps = data.tasks.filter((t) =>
numericDependencies.includes(t.id)
);
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
// Add an overview of indirect dependencies if present
const indirectDeps = dependentTasks.filter(
(t) => !numericDependencies.includes(t.id)
);
if (indirectDeps.length > 0) {
contextTasks += `\n\nIndirect dependencies (dependencies of dependencies):`;
contextTasks += `\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
if (indirectDeps.length > 5) {
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
}
}
// Add more details about each dependency, prioritizing direct dependencies
contextTasks += `\n\nDetailed information about dependencies:`;
for (const depTask of uniqueDetailedTasks) {
const depthInfo = depthMap.get(depTask.id)
? ` (depth: ${depthMap.get(depTask.id)})`
: '';
const isDirect = numericDependencies.includes(depTask.id)
? ' [DIRECT DEPENDENCY]'
: '';
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
contextTasks += `Description: ${depTask.description}\n`;
contextTasks += `Status: ${depTask.status || 'pending'}\n`;
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`;
// List its dependencies
if (depTask.dependencies && depTask.dependencies.length > 0) {
const depDeps = depTask.dependencies.map((dId) => {
const depDepTask = data.tasks.find((t) => t.id === dId);
return depDepTask
? `Task ${dId}: ${depDepTask.title}`
: `Task ${dId}`;
});
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
} else {
contextTasks += `Dependencies: None\n`;
}
// Add implementation details but truncate if too long
if (depTask.details) {
const truncatedDetails =
depTask.details.length > 400
? depTask.details.substring(0, 400) + '... (truncated)'
: depTask.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
}
// Add dependency chain visualization
if (dependencyGraphs.length > 0) {
contextTasks += '\n\nDependency Chain Visualization:';
// Helper function to format dependency chain as text
function formatDependencyChain(
node,
prefix = '',
isLast = true,
depth = 0
) {
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '│ ';
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
if (node.dependencies && node.dependencies.length > 0) {
for (let i = 0; i < node.dependencies.length; i++) {
const isLastChild = i === node.dependencies.length - 1;
result += formatDependencyChain(
node.dependencies[i],
prefix + childPrefix,
isLastChild,
depth + 1
);
}
}
return result;
}
// Format each dependency graph
for (const graph of dependencyGraphs) {
contextTasks += formatDependencyChain(graph);
}
}
// Show dependency analysis in CLI mode
if (outputFormat === 'text') {
if (directDeps.length > 0) {
console.log(chalk.gray(` Explicitly specified dependencies:`));
directDeps.forEach((t) => {
console.log(
chalk.yellow(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
if (indirectDeps.length > 0) {
console.log(
chalk.gray(
`\n Indirect dependencies (${indirectDeps.length} total):`
)
);
indirectDeps.slice(0, 3).forEach((t) => {
const depth = depthMap.get(t.id) || 0;
console.log(
chalk.cyan(
` • Task ${t.id} [depth ${depth}]: ${truncate(t.title, 45)}`
)
);
});
if (indirectDeps.length > 3) {
console.log(
chalk.cyan(
` • ... and ${indirectDeps.length - 3} more indirect dependencies`
)
);
}
}
// Visualize the dependency chain
if (dependencyGraphs.length > 0) {
console.log(chalk.gray(`\n Dependency chain visualization:`));
// Convert dependency graph to ASCII art for terminal
function visualizeDependencyGraph(
node,
prefix = '',
isLast = true,
depth = 0
) {
if (depth > 2) return; // Limit depth for display
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '│ ';
console.log(
chalk.blue(
` ${prefix}${connector}Task ${node.id}: ${truncate(node.title, 40)}`
)
);
if (node.dependencies && node.dependencies.length > 0) {
for (let i = 0; i < node.dependencies.length; i++) {
const isLastChild = i === node.dependencies.length - 1;
visualizeDependencyGraph(
node.dependencies[i],
prefix + childPrefix,
isLastChild,
depth + 1
);
}
}
}
// Visualize each dependency graph
for (const graph of dependencyGraphs) {
visualizeDependencyGraph(graph);
}
}
console.log(); // Add spacing
}
} else {
// If no dependencies provided, use Fuse.js to find semantically related tasks
// Create fuzzy search index for all tasks
const searchOptions = {
includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [
{ name: 'title', weight: 1.5 }, // Title is most important
{ name: 'description', weight: 2 }, // Description is very important
{ name: 'details', weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things
{ name: 'dependencyTitles', weight: 0.5 }
],
// Sort matches by score (lower is better)
shouldSort: true,
// Allow searching in nested properties
useExtendedSearch: true,
// Return up to 50 matches
limit: 50
};
// Prepare task data with dependencies expanded as titles for better semantic search
const searchableTasks = data.tasks.map((task) => {
// Get titles of this task's dependencies if they exist
const dependencyTitles =
task.dependencies?.length > 0
? task.dependencies
.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask ? depTask.title : '';
})
.filter((title) => title)
.join(' ')
: '';
return {
...task,
dependencyTitles
};
});
// Create search index using Fuse.js
const fuse = new Fuse(searchableTasks, searchOptions);
// Extract significant words and phrases from the prompt
const promptWords = prompt
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
// Use the user's prompt for fuzzy search
const fuzzyResults = fuse.search(prompt);
// Also search for each significant word to catch different aspects
let wordResults = [];
for (const word of promptWords) {
if (word.length > 5) {
// Only use significant words
const results = fuse.search(word);
if (results.length > 0) {
wordResults.push(...results);
}
}
}
// Merge and deduplicate results
const mergedResults = [...fuzzyResults];
// Add word results that aren't already in fuzzyResults
for (const wordResult of wordResults) {
if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
mergedResults.push(wordResult);
}
}
// Group search results by relevance
const highRelevance = mergedResults
.filter((result) => result.score < 0.25)
.map((result) => result.item);
const mediumRelevance = mergedResults
.filter((result) => result.score >= 0.25 && result.score < 0.4)
.map((result) => result.item);
// Get recent tasks (newest first)
const recentTasks = [...data.tasks]
.sort((a, b) => b.id - a.id)
.slice(0, 5);
// Combine high relevance, medium relevance, and recent tasks
// Prioritize high relevance first
const allRelevantTasks = [...highRelevance];
// Add medium relevance if not already included
for (const task of mediumRelevance) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add recent tasks if not already included
for (const task of recentTasks) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Get top N results for context
const relatedTasks = allRelevantTasks.slice(0, 8);
// Format basic task overviews
if (relatedTasks.length > 0) {
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
.map((t, i) => {
const relevanceMarker = i < highRelevance.length ? '⭐ ' : '';
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
})
.join('\n')}`;
}
if (
recentTasks.length > 0 &&
!contextTasks.includes('Recently created tasks')
) {
contextTasks += `\n\nRecently created tasks:\n${recentTasks
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
}
// Add detailed information about the most relevant tasks
const allDetailedTasks = [...relatedTasks.slice(0, 25)];
uniqueDetailedTasks = Array.from(
new Map(allDetailedTasks.map((t) => [t.id, t])).values()
).slice(0, 20);
if (uniqueDetailedTasks.length > 0) {
contextTasks += `\n\nDetailed information about relevant tasks:`;
for (const task of uniqueDetailedTasks) {
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
contextTasks += `Description: ${task.description}\n`;
contextTasks += `Status: ${task.status || 'pending'}\n`;
contextTasks += `Priority: ${task.priority || 'medium'}\n`;
if (task.dependencies && task.dependencies.length > 0) {
// Format dependency list with titles
const depList = task.dependencies.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask
? `Task ${depId} (${depTask.title})`
: `Task ${depId}`;
});
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
}
// Add implementation details but truncate if too long
if (task.details) {
const truncatedDetails =
task.details.length > 400
? task.details.substring(0, 400) + '... (truncated)'
: task.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
}
}
// Add a concise view of the task dependency structure
contextTasks += '\n\nSummary of task dependencies in the project:';
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
// Prioritize tasks from our similarity search
const relevantTaskIds = new Set(uniqueDetailedTasks.map((t) => t.id));
const relevantPendingTasks = data.tasks
.filter(
(t) =>
(t.status === 'pending' || t.status === 'in-progress') &&
// Either in our relevant set OR has relevant words in title/description
(relevantTaskIds.has(t.id) ||
promptWords.some(
(word) =>
t.title.toLowerCase().includes(word) ||
t.description.toLowerCase().includes(word)
))
)
.slice(0, 10);
for (const task of relevantPendingTasks) {
const depsStr =
task.dependencies && task.dependencies.length > 0
? task.dependencies.join(', ')
: 'None';
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
}
// Additional analysis of common patterns
const similarPurposeTasks = data.tasks.filter((t) =>
prompt.toLowerCase().includes(t.title.toLowerCase())
);
let commonDeps = []; // Initialize commonDeps
if (similarPurposeTasks.length > 0) {
contextTasks += `\n\nCommon patterns for similar tasks:`;
// Collect dependencies from similar purpose tasks
const similarDeps = similarPurposeTasks
.filter((t) => t.dependencies && t.dependencies.length > 0)
.map((t) => t.dependencies)
.flat();
// Count frequency of each dependency
const depCounts = {};
similarDeps.forEach((dep) => {
depCounts[dep] = (depCounts[dep] || 0) + 1;
});
// Get most common dependencies for similar tasks
commonDeps = Object.entries(depCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
if (commonDeps.length > 0) {
contextTasks += '\nMost common dependencies for similar tasks:';
commonDeps.forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
contextTasks += `\n- Task ${depId} (used by ${count} similar tasks): ${depTask.title}`;
}
});
}
}
// Show fuzzy search analysis in CLI mode
if (outputFormat === 'text') {
console.log(
chalk.gray(
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
)
);
if (highRelevance.length > 0) {
console.log(
chalk.gray(`\n High relevance matches (score < 0.25):`)
);
highRelevance.slice(0, 25).forEach((t) => {
console.log(
chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
if (mediumRelevance.length > 0) {
console.log(
chalk.gray(`\n Medium relevance matches (score < 0.4):`)
);
mediumRelevance.slice(0, 10).forEach((t) => {
console.log(
chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
// Show dependency patterns
if (commonDeps && commonDeps.length > 0) {
console.log(
chalk.gray(`\n Common dependency patterns for similar tasks:`)
);
commonDeps.slice(0, 3).forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
console.log(
chalk.blue(
` • Task ${depId} (${count}x): ${truncate(depTask.title, 45)}`
)
);
}
});
}
// Add information about which tasks will be provided in detail
if (uniqueDetailedTasks.length > 0) {
console.log(
chalk.gray(
`\n Providing detailed context for ${uniqueDetailedTasks.length} most relevant tasks:`
)
);
uniqueDetailedTasks.forEach((t) => {
const isHighRelevance = highRelevance.some(
(ht) => ht.id === t.id
);
const relevanceIndicator = isHighRelevance ? '⭐ ' : '';
console.log(
chalk.cyan(
`${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
)
);
});
}
console.log(); // Add spacing
}
}
// DETERMINE THE ACTUAL COUNT OF DETAILED TASKS BEING USED FOR AI CONTEXT
let actualDetailedTasksCount = 0;
if (numericDependencies.length > 0) {
// In explicit dependency mode, we used 'uniqueDetailedTasks' derived from 'dependentTasks'
// Ensure 'uniqueDetailedTasks' from THAT scope is used or re-evaluate.
// For simplicity, let's assume 'dependentTasks' reflects the detailed tasks.
actualDetailedTasksCount = dependentTasks.length;
} else {
// In fuzzy search mode, 'uniqueDetailedTasks' from THIS scope is correct.
actualDetailedTasksCount = uniqueDetailedTasks
? uniqueDetailedTasks.length
: 0;
}
// Add a visual transition to show we're moving to AI generation - only for CLI
if (outputFormat === 'text') { if (outputFormat === 'text') {
console.log( console.log(
boxen( boxen(
chalk.white.bold('AI Task Generation') + chalk.white.bold('AI Task Generation') +
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` + `\n\n${chalk.gray('Analyzing context and generating task details using AI...')}`,
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` +
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(
numericDependencies.length > 0
? dependentTasks.length // Use length of tasks from explicit dependency path
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
)}`,
{ {
padding: { top: 0, bottom: 1, left: 1, right: 1 }, padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 }, margin: { top: 1, bottom: 0 },
@@ -825,7 +295,6 @@ async function addTask(
} }
) )
); );
console.log(); // Add spacing
} }
// System Prompt - Enhanced for dependency awareness // System Prompt - Enhanced for dependency awareness
@@ -866,8 +335,7 @@ async function addTask(
// User Prompt // User Prompt
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
${contextTasks} ${gatheredContext}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on. Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
@@ -975,7 +443,9 @@ async function addTask(
if (taskData.dependencies?.length) { if (taskData.dependencies?.length) {
const allValidDeps = taskData.dependencies.every((depId) => { const allValidDeps = taskData.dependencies.every((depId) => {
const numDepId = parseInt(depId, 10); const numDepId = parseInt(depId, 10);
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); return (
!Number.isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId)
);
}); });
if (!allValidDeps) { if (!allValidDeps) {
@@ -985,7 +455,9 @@ async function addTask(
); );
newTask.dependencies = taskData.dependencies.filter((depId) => { newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10); const numDepId = parseInt(depId, 10);
return !isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId); return (
!Number.isNaN(numDepId) && data.tasks.some((t) => t.id === numDepId)
);
}); });
} }
} }
@@ -1032,7 +504,6 @@ async function addTask(
return 'red'; return 'red';
case 'low': case 'low':
return 'gray'; return 'gray';
case 'medium':
default: default:
return 'yellow'; return 'yellow';
} }

View File

@@ -18,19 +18,32 @@ import {
COMPLEXITY_REPORT_FILE, COMPLEXITY_REPORT_FILE,
LEGACY_TASKS_FILE LEGACY_TASKS_FILE
} from '../../../src/constants/paths.js'; } from '../../../src/constants/paths.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks } from '../utils.js';
/** /**
* Generates the prompt for complexity analysis. * Generates the prompt for complexity analysis.
* (Moved from ai-services.js and simplified) * (Moved from ai-services.js and simplified)
* @param {Object} tasksData - The tasks data object. * @param {Object} tasksData - The tasks data object.
* @param {string} [gatheredContext] - The gathered context for the analysis.
* @returns {string} The generated prompt. * @returns {string} The generated prompt.
*/ */
function generateInternalComplexityAnalysisPrompt(tasksData) { function generateInternalComplexityAnalysisPrompt(
tasksData,
gatheredContext = ''
) {
const tasksString = JSON.stringify(tasksData.tasks, null, 2); const tasksString = JSON.stringify(tasksData.tasks, null, 2);
return `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each. let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.
Tasks: Tasks:
${tasksString} ${tasksString}`;
if (gatheredContext) {
prompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
prompt += `
Respond ONLY with a valid JSON array matching the schema: Respond ONLY with a valid JSON array matching the schema:
[ [
@@ -46,6 +59,7 @@ Respond ONLY with a valid JSON array matching the schema:
] ]
Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`; Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
return prompt;
} }
/** /**
@@ -200,6 +214,41 @@ async function analyzeTaskComplexity(options, context = {}) {
}; };
} }
// --- Context Gathering ---
let gatheredContext = '';
if (originalData && originalData.tasks.length > 0) {
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
const fuzzySearch = new FuzzyTaskSearch(
allTasksFlat,
'analyze-complexity'
);
// Create a query from the tasks being analyzed
const searchQuery = tasksData.tasks
.map((t) => `${t.title} ${t.description}`)
.join(' ');
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 10
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
if (relevantTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: relevantTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
reportLog(
`Could not gather additional context: ${contextError.message}`,
'warn'
);
}
}
// --- End Context Gathering ---
const skippedCount = originalTaskCount - tasksData.tasks.length; const skippedCount = originalTaskCount - tasksData.tasks.length;
reportLog( reportLog(
`Found ${originalTaskCount} total tasks in the task file.`, `Found ${originalTaskCount} total tasks in the task file.`,
@@ -226,7 +275,7 @@ async function analyzeTaskComplexity(options, context = {}) {
// Check for existing report before doing analysis // Check for existing report before doing analysis
let existingReport = null; let existingReport = null;
let existingAnalysisMap = new Map(); // For quick lookups by task ID const existingAnalysisMap = new Map(); // For quick lookups by task ID
try { try {
if (fs.existsSync(outputPath)) { if (fs.existsSync(outputPath)) {
existingReport = readJSON(outputPath); existingReport = readJSON(outputPath);
@@ -342,7 +391,10 @@ async function analyzeTaskComplexity(options, context = {}) {
} }
// Continue with regular analysis path // Continue with regular analysis path
const prompt = generateInternalComplexityAnalysisPrompt(tasksData); const prompt = generateInternalComplexityAnalysisPrompt(
tasksData,
gatheredContext
);
const systemPrompt = const systemPrompt =
'You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.'; 'You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.';

View File

@@ -1,4 +1,4 @@
import { log, readJSON, isSilentMode } from '../utils.js'; import { log, readJSON, isSilentMode, findProjectRoot } from '../utils.js';
import { import {
startLoadingIndicator, startLoadingIndicator,
stopLoadingIndicator, stopLoadingIndicator,
@@ -32,9 +32,14 @@ async function expandAllTasks(
context = {}, context = {},
outputFormat = 'text' // Assume text default for CLI outputFormat = 'text' // Assume text default for CLI
) { ) {
const { session, mcpLog } = context; const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const isMCPCall = !!mcpLog; // Determine if called from MCP const isMCPCall = !!mcpLog; // Determine if called from MCP
const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
// Use mcpLog if available, otherwise use the default console log wrapper respecting silent mode // Use mcpLog if available, otherwise use the default console log wrapper respecting silent mode
const logger = const logger =
mcpLog || mcpLog ||
@@ -69,7 +74,7 @@ async function expandAllTasks(
try { try {
logger.info(`Reading tasks from ${tasksPath}`); logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath); const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) { if (!data || !data.tasks) {
throw new Error(`Invalid tasks data in ${tasksPath}`); throw new Error(`Invalid tasks data in ${tasksPath}`);
} }
@@ -119,7 +124,7 @@ async function expandAllTasks(
numSubtasks, numSubtasks,
useResearch, useResearch,
additionalContext, additionalContext,
context, // Pass the whole context object { session, mcpLog } { ...context, projectRoot }, // Pass the whole context object with projectRoot
force force
); );
expandedCount++; expandedCount++;

View File

@@ -15,6 +15,9 @@ import { generateTextService } from '../ai-services-unified.js';
import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js'; import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from './generate-task-files.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js'; import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// --- Zod Schemas (Keep from previous step) --- // --- Zod Schemas (Keep from previous step) ---
const subtaskSchema = z const subtaskSchema = z
@@ -285,9 +288,9 @@ function parseSubtasksFromText(
const patternStartIndex = jsonToParse.indexOf(targetPattern); const patternStartIndex = jsonToParse.indexOf(targetPattern);
if (patternStartIndex !== -1) { if (patternStartIndex !== -1) {
let openBraces = 0; const openBraces = 0;
let firstBraceFound = false; const firstBraceFound = false;
let extractedJsonBlock = ''; const extractedJsonBlock = '';
// ... (loop for brace counting as before) ... // ... (loop for brace counting as before) ...
// ... (if successful, jsonToParse = extractedJsonBlock) ... // ... (if successful, jsonToParse = extractedJsonBlock) ...
// ... (if that fails, fallbacks as before) ... // ... (if that fails, fallbacks as before) ...
@@ -349,7 +352,8 @@ function parseSubtasksFromText(
? rawSubtask.dependencies ? rawSubtask.dependencies
.map((dep) => (typeof dep === 'string' ? parseInt(dep, 10) : dep)) .map((dep) => (typeof dep === 'string' ? parseInt(dep, 10) : dep))
.filter( .filter(
(depId) => !isNaN(depId) && depId >= startId && depId < currentId (depId) =>
!Number.isNaN(depId) && depId >= startId && depId < currentId
) )
: [], : [],
status: 'pending' status: 'pending'
@@ -418,7 +422,9 @@ async function expandTask(
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath // Determine projectRoot: Use from context if available, otherwise derive from tasksPath
const projectRoot = const projectRoot =
contextProjectRoot || path.dirname(path.dirname(tasksPath)); contextProjectRoot ||
findProjectRoot() ||
path.dirname(path.dirname(tasksPath));
// Use mcpLog if available, otherwise use the default console log wrapper // Use mcpLog if available, otherwise use the default console log wrapper
const logger = mcpLog || { const logger = mcpLog || {
@@ -436,7 +442,7 @@ async function expandTask(
try { try {
// --- Task Loading/Filtering (Unchanged) --- // --- Task Loading/Filtering (Unchanged) ---
logger.info(`Reading tasks from ${tasksPath}`); logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath); const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) if (!data || !data.tasks)
throw new Error(`Invalid tasks data in ${tasksPath}`); throw new Error(`Invalid tasks data in ${tasksPath}`);
const taskIndex = data.tasks.findIndex( const taskIndex = data.tasks.findIndex(
@@ -458,6 +464,35 @@ async function expandTask(
} }
// --- End Force Flag Handling --- // --- End Force Flag Handling ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
const searchQuery = `${task.title} ${task.description}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([taskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
logger.warn(`Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
// --- Complexity Report Integration --- // --- Complexity Report Integration ---
let finalSubtaskCount; let finalSubtaskCount;
let promptContent = ''; let promptContent = '';
@@ -498,7 +533,7 @@ async function expandTask(
// Determine final subtask count // Determine final subtask count
const explicitNumSubtasks = parseInt(numSubtasks, 10); const explicitNumSubtasks = parseInt(numSubtasks, 10);
if (!isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) { if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
finalSubtaskCount = explicitNumSubtasks; finalSubtaskCount = explicitNumSubtasks;
logger.info( logger.info(
`Using explicitly provided subtask count: ${finalSubtaskCount}` `Using explicitly provided subtask count: ${finalSubtaskCount}`
@@ -512,7 +547,7 @@ async function expandTask(
finalSubtaskCount = getDefaultSubtasks(session); finalSubtaskCount = getDefaultSubtasks(session);
logger.info(`Using default number of subtasks: ${finalSubtaskCount}`); logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
} }
if (isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) { if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
logger.warn( logger.warn(
`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.` `Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
); );
@@ -528,6 +563,9 @@ async function expandTask(
// Append additional context and reasoning // Append additional context and reasoning
promptContent += `\n\n${additionalContext}`.trim(); promptContent += `\n\n${additionalContext}`.trim();
promptContent += `${complexityReasoningContext}`.trim(); promptContent += `${complexityReasoningContext}`.trim();
if (gatheredContext) {
promptContent += `\n\n# Project Context\n\n${gatheredContext}`;
}
// --- Use Simplified System Prompt for Report Prompts --- // --- Use Simplified System Prompt for Report Prompts ---
systemPrompt = `You are an AI assistant helping with task breakdown. Generate exactly ${finalSubtaskCount} subtasks based on the provided prompt and context. Respond ONLY with a valid JSON object containing a single key "subtasks" whose value is an array of the generated subtask objects. Each subtask object in the array must have keys: "id", "title", "description", "dependencies", "details", "status". Ensure the 'id' starts from ${nextSubtaskId} and is sequential. Ensure 'dependencies' only reference valid prior subtask IDs generated in this response (starting from ${nextSubtaskId}). Ensure 'status' is 'pending'. Do not include any other text or explanation.`; systemPrompt = `You are an AI assistant helping with task breakdown. Generate exactly ${finalSubtaskCount} subtasks based on the provided prompt and context. Respond ONLY with a valid JSON object containing a single key "subtasks" whose value is an array of the generated subtask objects. Each subtask object in the array must have keys: "id", "title", "description", "dependencies", "details", "status". Ensure the 'id' starts from ${nextSubtaskId} and is sequential. Ensure 'dependencies' only reference valid prior subtask IDs generated in this response (starting from ${nextSubtaskId}). Ensure 'status' is 'pending'. Do not include any other text or explanation.`;
@@ -537,8 +575,13 @@ async function expandTask(
// --- End Simplified System Prompt --- // --- End Simplified System Prompt ---
} else { } else {
// Use standard prompt generation // Use standard prompt generation
const combinedAdditionalContext = let combinedAdditionalContext =
`${additionalContext}${complexityReasoningContext}`.trim(); `${additionalContext}${complexityReasoningContext}`.trim();
if (gatheredContext) {
combinedAdditionalContext =
`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
}
if (useResearch) { if (useResearch) {
promptContent = generateResearchUserPrompt( promptContent = generateResearchUserPrompt(
task, task,

View File

@@ -11,7 +11,12 @@ import { highlight } from 'cli-highlight';
import { ContextGatherer } from '../utils/contextGatherer.js'; import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js'; import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { generateTextService } from '../ai-services-unified.js'; import { generateTextService } from '../ai-services-unified.js';
import { log as consoleLog, findProjectRoot, readJSON } from '../utils.js'; import {
log as consoleLog,
findProjectRoot,
readJSON,
flattenTasksWithSubtasks
} from '../utils.js';
import { import {
displayAiUsageSummary, displayAiUsageSummary,
startLoadingIndicator, startLoadingIndicator,
@@ -579,42 +584,6 @@ function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
console.log(chalk.green('✅ Research completed')); console.log(chalk.green('✅ Research completed'));
} }
/**
* Flatten tasks array to include subtasks as individual searchable items
* @param {Array} tasks - Array of task objects
* @returns {Array} Flattened array including both tasks and subtasks
*/
function flattenTasksWithSubtasks(tasks) {
const flattened = [];
for (const task of tasks) {
// Add the main task
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
flattened.push({
...subtask,
searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
isSubtask: true,
parentId: task.id,
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
});
}
}
}
return flattened;
}
/** /**
* Handle follow-up questions in interactive mode * Handle follow-up questions in interactive mode
* @param {Object} originalOptions - Original research options * @param {Object} originalOptions - Original research options

View File

@@ -15,11 +15,15 @@ import {
readJSON, readJSON,
writeJSON, writeJSON,
truncate, truncate,
isSilentMode isSilentMode,
findProjectRoot,
flattenTasksWithSubtasks
} from '../utils.js'; } from '../utils.js';
import { generateTextService } from '../ai-services-unified.js'; import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js'; import { getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from './generate-task-files.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
/** /**
* Update a subtask by appending additional timestamped information using the unified AI service. * Update a subtask by appending additional timestamped information using the unified AI service.
@@ -42,7 +46,7 @@ async function updateSubtaskById(
context = {}, context = {},
outputFormat = context.mcpLog ? 'json' : 'text' outputFormat = context.mcpLog ? 'json' : 'text'
) { ) {
const { session, mcpLog, projectRoot } = context; const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog; const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog; const isMCP = !!mcpLog;
@@ -81,7 +85,12 @@ async function updateSubtaskById(
throw new Error(`Tasks file not found at path: ${tasksPath}`); throw new Error(`Tasks file not found at path: ${tasksPath}`);
} }
const data = readJSON(tasksPath); const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) { if (!data || !data.tasks) {
throw new Error( throw new Error(
`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.` `No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`
@@ -93,9 +102,9 @@ async function updateSubtaskById(
const subtaskIdNum = parseInt(subtaskIdStr, 10); const subtaskIdNum = parseInt(subtaskIdStr, 10);
if ( if (
isNaN(parentId) || Number.isNaN(parentId) ||
parentId <= 0 || parentId <= 0 ||
isNaN(subtaskIdNum) || Number.isNaN(subtaskIdNum) ||
subtaskIdNum <= 0 subtaskIdNum <= 0
) { ) {
throw new Error( throw new Error(
@@ -125,6 +134,35 @@ async function updateSubtaskById(
const subtask = parentTask.subtasks[subtaskIndex]; const subtask = parentTask.subtasks[subtaskIndex];
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask');
const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([subtaskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
report('warn', `Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
if (outputFormat === 'text') { if (outputFormat === 'text') {
const table = new Table({ const table = new Table({
head: [ head: [
@@ -200,7 +238,11 @@ Output Requirements:
4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with "Okay, here's the update...").`; 4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with "Okay, here's the update...").`;
// Pass the existing subtask.details in the user prompt for the AI's context. // Pass the existing subtask.details in the user prompt for the AI's context.
const userPrompt = `Task Context:\n${contextString}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.`; let userPrompt = `Task Context:\n${contextString}\n\nUser Request: "${prompt}"\n\nBased on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.`;
if (gatheredContext) {
userPrompt += `\n\n# Additional Project Context\n\n${gatheredContext}`;
}
const role = useResearch ? 'research' : 'main'; const role = useResearch ? 'research' : 'main';
report('info', `Using AI text service with role: ${role}`); report('info', `Using AI text service with role: ${role}`);

View File

@@ -10,7 +10,9 @@ import {
readJSON, readJSON,
writeJSON, writeJSON,
truncate, truncate,
isSilentMode isSilentMode,
flattenTasksWithSubtasks,
findProjectRoot
} from '../utils.js'; } from '../utils.js';
import { import {
@@ -26,6 +28,8 @@ import {
isApiKeySet // Keep this check isApiKeySet // Keep this check
} from '../config-manager.js'; } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from './generate-task-files.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
// Zod schema for post-parsing validation of the updated task object // Zod schema for post-parsing validation of the updated task object
const updatedTaskSchema = z const updatedTaskSchema = z
@@ -216,7 +220,7 @@ async function updateTaskById(
context = {}, context = {},
outputFormat = 'text' outputFormat = 'text'
) { ) {
const { session, mcpLog, projectRoot } = context; const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog; const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog; const isMCP = !!mcpLog;
@@ -255,8 +259,14 @@ async function updateTaskById(
throw new Error(`Tasks file not found: ${tasksPath}`); throw new Error(`Tasks file not found: ${tasksPath}`);
// --- End Input Validations --- // --- End Input Validations ---
// Determine project root
const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
// --- Task Loading and Status Check (Keep existing) --- // --- Task Loading and Status Check (Keep existing) ---
const data = readJSON(tasksPath); const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}.`); throw new Error(`No valid tasks found in ${tasksPath}.`);
const taskIndex = data.tasks.findIndex((task) => task.id === taskId); const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
@@ -293,6 +303,35 @@ async function updateTaskById(
} }
// --- End Task Loading --- // --- End Task Loading ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([taskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult;
}
} catch (contextError) {
report('warn', `Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
// --- Display Task Info (CLI Only - Keep existing) --- // --- Display Task Info (CLI Only - Keep existing) ---
if (outputFormat === 'text') { if (outputFormat === 'text') {
// Show the task that will be updated // Show the task that will be updated
@@ -370,7 +409,13 @@ Guidelines:
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`; The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
const taskDataString = JSON.stringify(taskToUpdate, null, 2); // Use original task data const taskDataString = JSON.stringify(taskToUpdate, null, 2); // Use original task data
const userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated task as a valid JSON object.`; let userPrompt = `Here is the task to update:\n${taskDataString}\n\nPlease update this task based on the following new context:\n${prompt}\n\nIMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
if (gatheredContext) {
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
userPrompt += `\n\nReturn only the updated task as a valid JSON object.`;
// --- End Build Prompts --- // --- End Build Prompts ---
let loadingIndicator = null; let loadingIndicator = null;

View File

@@ -23,6 +23,9 @@ import { getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from './generate-task-files.js';
import { generateTextService } from '../ai-services-unified.js'; import { generateTextService } from '../ai-services-unified.js';
import { getModelConfiguration } from './models.js'; import { getModelConfiguration } from './models.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// Zod schema for validating the structure of tasks AFTER parsing // Zod schema for validating the structure of tasks AFTER parsing
const updatedTaskSchema = z const updatedTaskSchema = z
@@ -228,7 +231,7 @@ async function updateTasks(
context = {}, context = {},
outputFormat = 'text' // Default to text for CLI outputFormat = 'text' // Default to text for CLI
) { ) {
const { session, mcpLog, projectRoot } = context; const { session, mcpLog, projectRoot: providedProjectRoot } = context;
// Use mcpLog if available, otherwise use the imported consoleLog function // Use mcpLog if available, otherwise use the imported consoleLog function
const logFn = mcpLog || consoleLog; const logFn = mcpLog || consoleLog;
// Flag to easily check which logger type we have // Flag to easily check which logger type we have
@@ -246,8 +249,14 @@ async function updateTasks(
`Updating tasks from ID ${fromId} with prompt: "${prompt}"` `Updating tasks from ID ${fromId} with prompt: "${prompt}"`
); );
// Determine project root
const projectRoot = providedProjectRoot || findProjectRoot();
if (!projectRoot) {
throw new Error('Could not determine project root directory');
}
// --- Task Loading/Filtering (Unchanged) --- // --- Task Loading/Filtering (Unchanged) ---
const data = readJSON(tasksPath); const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}`); throw new Error(`No valid tasks found in ${tasksPath}`);
const tasksToUpdate = data.tasks.filter( const tasksToUpdate = data.tasks.filter(
@@ -263,6 +272,38 @@ async function updateTasks(
} }
// --- End Task Loading/Filtering --- // --- End Task Loading/Filtering ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
const searchResults = fuzzySearch.findRelevantTasks(prompt, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString());
const finalTaskIds = [
...new Set([...tasksToUpdateIds, ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult; // contextResult is a string
}
} catch (contextError) {
logFn(
'warn',
`Could not gather additional context: ${contextError.message}`
);
}
// --- End Context Gathering ---
// --- Display Tasks to Update (CLI Only - Unchanged) --- // --- Display Tasks to Update (CLI Only - Unchanged) ---
if (outputFormat === 'text') { if (outputFormat === 'text') {
// Show the tasks that will be updated // Show the tasks that will be updated
@@ -344,7 +385,13 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
// Keep the original user prompt logic // Keep the original user prompt logic
const taskDataString = JSON.stringify(tasksToUpdate, null, 2); const taskDataString = JSON.stringify(tasksToUpdate, null, 2);
const userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.\n\nReturn only the updated tasks as a valid JSON array.`; let userPrompt = `Here are the tasks to update:\n${taskDataString}\n\nPlease update these tasks based on the following new context:\n${prompt}\n\nIMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "status": "completed" should be preserved exactly as is. Build your changes around these completed items.`;
if (gatheredContext) {
userPrompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
userPrompt += `\n\nReturn only the updated tasks as a valid JSON array.`;
// --- End Build Prompts --- // --- End Build Prompts ---
// --- AI Call --- // --- AI Call ---

View File

@@ -2543,6 +2543,98 @@ async function displayMultipleTasksSummary(
} }
} }
/**
* Display context analysis results with beautiful formatting
* @param {Object} analysisData - Analysis data from ContextGatherer
* @param {string} semanticQuery - The original query used for semantic search
* @param {number} contextSize - Size of gathered context in characters
*/
function displayContextAnalysis(analysisData, semanticQuery, contextSize) {
if (isSilentMode() || !analysisData) return;
const { highRelevance, mediumRelevance, recentTasks, allRelevantTasks } =
analysisData;
// Create the context analysis display
let analysisContent = chalk.white.bold('Context Analysis') + '\n\n';
// Query info
analysisContent +=
chalk.gray('Query: ') + chalk.white(`"${semanticQuery}"`) + '\n';
analysisContent +=
chalk.gray('Context size: ') +
chalk.cyan(`${contextSize.toLocaleString()} characters`) +
'\n';
analysisContent +=
chalk.gray('Tasks found: ') +
chalk.yellow(`${allRelevantTasks.length} relevant tasks`) +
'\n\n';
// High relevance matches
if (highRelevance.length > 0) {
analysisContent += chalk.green.bold('🎯 High Relevance Matches:') + '\n';
highRelevance.slice(0, 3).forEach((task) => {
analysisContent +=
chalk.green(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
});
if (highRelevance.length > 3) {
analysisContent +=
chalk.green(
` • ... and ${highRelevance.length - 3} more high relevance tasks`
) + '\n';
}
analysisContent += '\n';
}
// Medium relevance matches
if (mediumRelevance.length > 0) {
analysisContent += chalk.yellow.bold('📋 Medium Relevance Matches:') + '\n';
mediumRelevance.slice(0, 3).forEach((task) => {
analysisContent +=
chalk.yellow(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
});
if (mediumRelevance.length > 3) {
analysisContent +=
chalk.yellow(
` • ... and ${mediumRelevance.length - 3} more medium relevance tasks`
) + '\n';
}
analysisContent += '\n';
}
// Recent tasks (if they contributed)
const recentTasksNotInRelevance = recentTasks.filter(
(task) =>
!highRelevance.some((hr) => hr.id === task.id) &&
!mediumRelevance.some((mr) => mr.id === task.id)
);
if (recentTasksNotInRelevance.length > 0) {
analysisContent += chalk.cyan.bold('🕒 Recent Tasks (for context):') + '\n';
recentTasksNotInRelevance.slice(0, 2).forEach((task) => {
analysisContent +=
chalk.cyan(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
});
if (recentTasksNotInRelevance.length > 2) {
analysisContent +=
chalk.cyan(
` • ... and ${recentTasksNotInRelevance.length - 2} more recent tasks`
) + '\n';
}
}
console.log(
boxen(analysisContent, {
padding: { top: 1, bottom: 1, left: 2, right: 2 },
margin: { top: 1, bottom: 0 },
borderStyle: 'round',
borderColor: 'blue',
title: chalk.blue('🔍 Context Gathering'),
titleAlignment: 'center'
})
);
}
// Export UI functions // Export UI functions
export { export {
displayBanner, displayBanner,
@@ -2567,5 +2659,6 @@ export {
succeedLoadingIndicator, succeedLoadingIndicator,
failLoadingIndicator, failLoadingIndicator,
warnLoadingIndicator, warnLoadingIndicator,
infoLoadingIndicator infoLoadingIndicator,
displayContextAnalysis
}; };

View File

@@ -194,11 +194,12 @@ function log(level, ...args) {
} }
/** /**
* Reads and parses a JSON file with automatic tag migration for tasks.json * Reads and parses a JSON file
* @param {string} filepath - Path to the JSON file * @param {string} filepath - Path to the JSON file
* @returns {Object|null} Parsed JSON data or null if error occurs * @param {string} [projectRoot] - Optional project root for tag resolution (used by MCP)
* @returns {Object|null} The parsed JSON data or null if error
*/ */
function readJSON(filepath) { function readJSON(filepath, projectRoot = null) {
// GUARD: Prevent circular dependency during config loading // GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback let isDebug = false; // Default fallback
try { try {
@@ -211,30 +212,38 @@ function readJSON(filepath) {
} }
try { try {
if (!fs.existsSync(filepath)) {
if (isDebug) {
log('debug', `File not found: ${filepath}`);
}
return null;
}
const rawData = fs.readFileSync(filepath, 'utf8'); const rawData = fs.readFileSync(filepath, 'utf8');
let data = JSON.parse(rawData); let data = JSON.parse(rawData);
// Silent migration for tasks.json files: Transform old format to tagged format // Check if this is legacy tasks.json format that needs migration
// Only migrate if: 1) has "tasks" array at top level, 2) no "master" key exists
// 3) filepath indicates this is likely a tasks.json file
const isTasksFile =
filepath.includes('tasks.json') ||
path.basename(filepath) === 'tasks.json';
if ( if (
data && data &&
data.tasks && data.tasks &&
Array.isArray(data.tasks) && Array.isArray(data.tasks) &&
!data.master && !data.master &&
isTasksFile filepath.includes('tasks.json')
) { ) {
// Migrate from old format { "tasks": [...] } to new format { "master": { "tasks": [...] } } // This is legacy format - migrate to tagged format
const migratedData = { const migratedData = {
master: { master: {
tasks: data.tasks tasks: data.tasks
} }
}; };
// Copy any other top-level properties except 'tasks'
for (const [key, value] of Object.entries(data)) {
if (key !== 'tasks') {
migratedData[key] = value;
}
}
// Write the migrated format back using writeJSON for consistency // Write the migrated format back using writeJSON for consistency
try { try {
writeJSON(filepath, migratedData); writeJSON(filepath, migratedData);
@@ -282,28 +291,43 @@ function readJSON(filepath) {
); );
if (hasTaggedFormat) { if (hasTaggedFormat) {
// This is tagged format - resolve which tag to use // Default to master tag if anything goes wrong
// Derive project root from filepath to get correct tag context let resolvedTag = 'master';
const projectRoot =
findProjectRoot(path.dirname(filepath)) || path.dirname(filepath);
const resolvedTag = resolveTag({ projectRoot });
// Try to resolve the correct tag, but don't fail if it doesn't work
try {
if (projectRoot) {
// Use provided projectRoot
resolvedTag = resolveTag({ projectRoot });
} else {
// Try to derive projectRoot from filepath
const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
if (derivedProjectRoot) {
resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
}
// If derivedProjectRoot is null, stick with 'master' default
}
} catch (error) {
// If anything fails, just use master
resolvedTag = 'master';
}
// Return the tasks for the resolved tag, or master as fallback, or empty array
if (data[resolvedTag] && data[resolvedTag].tasks) { if (data[resolvedTag] && data[resolvedTag].tasks) {
// Return data in old format so existing code continues to work
data = { data = {
tag: resolvedTag, tag: resolvedTag,
tasks: data[resolvedTag].tasks, tasks: data[resolvedTag].tasks,
_rawTaggedData: data // Keep reference to full tagged data if needed _rawTaggedData: data
};
} else if (data.master && data.master.tasks) {
data = {
tag: 'master',
tasks: data.master.tasks,
_rawTaggedData: data
}; };
} else { } else {
// Tag doesn't exist, create empty tasks array and log warning // No valid tags found, return empty
if (isDebug) { data = { tasks: [], tag: 'master', _rawTaggedData: data };
log(
'warn',
`Tag "${resolvedTag}" not found in tasks file, using empty tasks array`
);
}
data = { tasks: [], tag: resolvedTag, _rawTaggedData: data };
} }
} }
} }
@@ -362,45 +386,41 @@ function performCompleteTagMigration(tasksJsonPath) {
*/ */
function migrateConfigJson(configPath) { function migrateConfigJson(configPath) {
try { try {
const configData = readJSON(configPath); const rawConfig = fs.readFileSync(configPath, 'utf8');
if (!configData) return; const config = JSON.parse(rawConfig);
if (!config) return;
let needsUpdate = false; let modified = false;
// Add defaultTag to global section if missing // Add global.defaultTag if missing
if (!configData.global) { if (!config.global) {
configData.global = {}; config.global = {};
} }
if (!config.global.defaultTag) {
if (!configData.global.defaultTag) { config.global.defaultTag = 'master';
configData.global.defaultTag = 'master'; modified = true;
needsUpdate = true;
} }
// Add tags section if missing // Add tags section if missing
if (!configData.tags) { if (!config.tags) {
configData.tags = { config.tags = {
autoSwitchOnBranch: false, enabledGitworkflow: false,
gitIntegration: { autoSwitchTagWithBranch: false
enabled: false,
autoSwitchTagWithBranch: false
}
}; };
needsUpdate = true; modified = true;
} }
if (needsUpdate) { if (modified) {
writeJSON(configPath, configData); fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
if (getDebugFlag()) { if (process.env.TASKMASTER_DEBUG === 'true') {
log( console.log(
'debug', '[DEBUG] Updated config.json with tagged task system settings'
`Migrated config.json with tagged task system configuration`
); );
} }
} }
} catch (error) { } catch (error) {
if (getDebugFlag()) { if (process.env.TASKMASTER_DEBUG === 'true') {
log('warn', `Error migrating config.json: ${error.message}`); console.warn(`[WARN] Error migrating config.json: ${error.message}`);
} }
} }
} }
@@ -418,13 +438,13 @@ function createStateJson(statePath) {
migrationNoticeShown: false migrationNoticeShown: false
}; };
writeJSON(statePath, initialState); fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
if (getDebugFlag()) { if (process.env.TASKMASTER_DEBUG === 'true') {
log('debug', `Created initial state.json for tagged task system`); console.log('[DEBUG] Created initial state.json for tagged task system');
} }
} catch (error) { } catch (error) {
if (getDebugFlag()) { if (process.env.TASKMASTER_DEBUG === 'true') {
log('warn', `Error creating state.json: ${error.message}`); console.warn(`[WARN] Error creating state.json: ${error.message}`);
} }
} }
} }
@@ -445,16 +465,27 @@ function markMigrationForNotice(tasksJsonPath) {
createStateJson(statePath); createStateJson(statePath);
} }
// Read and update state to mark migration occurred // Read and update state to mark migration occurred using fs directly
const stateData = readJSON(statePath) || {}; try {
if (stateData.migrationNoticeShown !== false) { const rawState = fs.readFileSync(statePath, 'utf8');
// Set to false to trigger notice display const stateData = JSON.parse(rawState) || {};
stateData.migrationNoticeShown = false; if (stateData.migrationNoticeShown !== false) {
writeJSON(statePath, stateData); // Set to false to trigger notice display
stateData.migrationNoticeShown = false;
fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), 'utf8');
}
} catch (stateError) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error updating state for migration notice: ${stateError.message}`
);
}
} }
} catch (error) { } catch (error) {
if (getDebugFlag()) { if (process.env.TASKMASTER_DEBUG === 'true') {
log('warn', `Error marking migration for notice: ${error.message}`); console.warn(
`[WARN] Error marking migration for notice: ${error.message}`
);
} }
} }
} }
@@ -898,15 +929,20 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
/** /**
* Gets the current tag from state.json or falls back to defaultTag from config * Gets the current tag from state.json or falls back to defaultTag from config
* @param {string} projectRoot - The project root directory * @param {string} projectRoot - The project root directory (required)
* @returns {string} The current tag name * @returns {string} The current tag name
*/ */
function getCurrentTag(projectRoot = process.cwd()) { function getCurrentTag(projectRoot) {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentTag');
}
try { try {
// Try to read current tag from state.json // Try to read current tag from state.json using fs directly
const statePath = path.join(projectRoot, '.taskmaster', 'state.json'); const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(statePath)) { if (fs.existsSync(statePath)) {
const stateData = JSON.parse(fs.readFileSync(statePath, 'utf8')); const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState);
if (stateData && stateData.currentTag) { if (stateData && stateData.currentTag) {
return stateData.currentTag; return stateData.currentTag;
} }
@@ -915,11 +951,12 @@ function getCurrentTag(projectRoot = process.cwd()) {
// Ignore errors, fall back to default // Ignore errors, fall back to default
} }
// Fall back to defaultTag from config or hardcoded default // Fall back to defaultTag from config using fs directly
try { try {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json'); const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); const rawConfig = fs.readFileSync(configPath, 'utf8');
const configData = JSON.parse(rawConfig);
if (configData && configData.global && configData.global.defaultTag) { if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag; return configData.global.defaultTag;
} }
@@ -933,19 +970,26 @@ function getCurrentTag(projectRoot = process.cwd()) {
} }
/** /**
* Resolves which tag to use based on context * Resolves the tag to use based on options
* @param {Object} options - Options object * @param {Object} options - Options object
* @param {string} [options.tag] - Explicit tag from --tag flag * @param {string} options.projectRoot - The project root directory (required)
* @param {string} [options.projectRoot] - Project root directory * @param {string} [options.tag] - Explicit tag to use
* @returns {string} The resolved tag name * @returns {string} The resolved tag name
*/ */
function resolveTag(options = {}) { function resolveTag(options = {}) {
// Priority: explicit tag > current tag from state > defaultTag from config > 'master' const { projectRoot, tag } = options;
if (options.tag) {
return options.tag; if (!projectRoot) {
throw new Error('projectRoot is required for resolveTag');
} }
return getCurrentTag(options.projectRoot); // If explicit tag provided, use it
if (tag) {
return tag;
}
// Otherwise get current tag from state/config
return getCurrentTag(projectRoot);
} }
/** /**
@@ -991,6 +1035,42 @@ function setTasksForTag(data, tagName, tasks) {
return data; return data;
} }
/**
* Flatten tasks array to include subtasks as individual searchable items
* @param {Array} tasks - Array of task objects
* @returns {Array} Flattened array including both tasks and subtasks
*/
function flattenTasksWithSubtasks(tasks) {
const flattened = [];
for (const task of tasks) {
// Add the main task
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
flattened.push({
...subtask,
searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
isSubtask: true,
parentId: task.id,
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
});
}
}
}
return flattened;
}
// Export all utility functions and configuration // Export all utility functions and configuration
export { export {
LOG_LEVELS, LOG_LEVELS,
@@ -1022,5 +1102,6 @@ export {
performCompleteTagMigration, performCompleteTagMigration,
migrateConfigJson, migrateConfigJson,
createStateJson, createStateJson,
markMigrationForNotice markMigrationForNotice,
flattenTasksWithSubtasks
}; };

View File

@@ -7,7 +7,13 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import pkg from 'gpt-tokens'; import pkg from 'gpt-tokens';
import { readJSON, findTaskById, truncate } from '../utils.js'; import Fuse from 'fuse.js';
import {
readJSON,
findTaskById,
truncate,
flattenTasksWithSubtasks
} from '../utils.js';
const { encode } = pkg; const { encode } = pkg;
@@ -17,7 +23,26 @@ const { encode } = pkg;
export class ContextGatherer { export class ContextGatherer {
constructor(projectRoot) { constructor(projectRoot) {
this.projectRoot = projectRoot; this.projectRoot = projectRoot;
this.tasksPath = path.join(projectRoot, 'tasks', 'tasks.json'); this.tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
this.allTasks = this._loadAllTasks();
}
_loadAllTasks() {
try {
const data = readJSON(this.tasksPath, this.projectRoot);
const tasks = data?.tasks || [];
return tasks;
} catch (error) {
console.warn(
`Warning: Could not load tasks for ContextGatherer: ${error.message}`
);
return [];
}
} }
/** /**
@@ -46,7 +71,10 @@ export class ContextGatherer {
* @param {string} [options.customContext] - Additional custom context * @param {string} [options.customContext] - Additional custom context
* @param {boolean} [options.includeProjectTree] - Include project file tree * @param {boolean} [options.includeProjectTree] - Include project file tree
* @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt' * @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt'
* @returns {Promise<string>} Formatted context string * @param {string} [options.semanticQuery] - A query string for semantic task searching.
* @param {number} [options.maxSemanticResults] - Max number of semantic results.
* @param {Array<number>} [options.dependencyTasks] - Array of task IDs to build dependency graphs from.
* @returns {Promise<Object>} Object with context string and analysis data
*/ */
async gather(options = {}) { async gather(options = {}) {
const { const {
@@ -55,86 +83,304 @@ export class ContextGatherer {
customContext = '', customContext = '',
includeProjectTree = false, includeProjectTree = false,
format = 'research', format = 'research',
includeTokenCounts = false semanticQuery,
maxSemanticResults = 10,
dependencyTasks = []
} = options; } = options;
const contextSections = []; const contextSections = [];
const tokenBreakdown = { const finalTaskIds = new Set(tasks.map(String));
customContext: null, let analysisData = null;
tasks: [],
files: [],
projectTree: null,
total: 0
};
// Add custom context first if provided // Semantic Search
if (customContext && customContext.trim()) { if (semanticQuery && this.allTasks.length > 0) {
const formattedCustom = this._formatCustomContext(customContext, format); const semanticResults = this._performSemanticSearch(
contextSections.push(formattedCustom); semanticQuery,
if (includeTokenCounts) { maxSemanticResults
tokenBreakdown.customContext = { );
tokens: this.countTokens(formattedCustom),
characters: formattedCustom.length // Store the analysis data for UI display
}; analysisData = semanticResults.analysisData;
}
semanticResults.tasks.forEach((task) => {
finalTaskIds.add(String(task.id));
});
} }
// Add task context // Dependency Graph Analysis
if (tasks.length > 0) { if (dependencyTasks.length > 0) {
const dependencyResults = this._buildDependencyContext(dependencyTasks);
dependencyResults.allRelatedTaskIds.forEach((id) =>
finalTaskIds.add(String(id))
);
// We can format and add dependencyResults.graphVisualization later if needed
}
// Add custom context first
if (customContext && customContext.trim()) {
contextSections.push(this._formatCustomContext(customContext, format));
}
// Gather context for the final list of tasks
if (finalTaskIds.size > 0) {
const taskContextResult = await this._gatherTaskContext( const taskContextResult = await this._gatherTaskContext(
tasks, Array.from(finalTaskIds),
format, format
includeTokenCounts
); );
if (taskContextResult.context) { if (taskContextResult.context) {
contextSections.push(taskContextResult.context); contextSections.push(taskContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.tasks = taskContextResult.breakdown;
}
} }
} }
// Add file context // Add file context
if (files.length > 0) { if (files.length > 0) {
const fileContextResult = await this._gatherFileContext( const fileContextResult = await this._gatherFileContext(files, format);
files,
format,
includeTokenCounts
);
if (fileContextResult.context) { if (fileContextResult.context) {
contextSections.push(fileContextResult.context); contextSections.push(fileContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.files = fileContextResult.breakdown;
}
} }
} }
// Add project tree context // Add project tree context
if (includeProjectTree) { if (includeProjectTree) {
const treeContextResult = await this._gatherProjectTreeContext( const treeContextResult = await this._gatherProjectTreeContext(format);
format,
includeTokenCounts
);
if (treeContextResult.context) { if (treeContextResult.context) {
contextSections.push(treeContextResult.context); contextSections.push(treeContextResult.context);
if (includeTokenCounts) { }
tokenBreakdown.projectTree = treeContextResult.breakdown; }
const finalContext = this._joinContextSections(contextSections, format);
return {
context: finalContext,
analysisData: analysisData,
contextSections: contextSections.length,
finalTaskIds: Array.from(finalTaskIds)
};
}
_performSemanticSearch(query, maxResults) {
const searchableTasks = this.allTasks.map((task) => {
const dependencyTitles =
task.dependencies?.length > 0
? task.dependencies
.map((depId) => this.allTasks.find((t) => t.id === depId)?.title)
.filter(Boolean)
.join(' ')
: '';
return { ...task, dependencyTitles };
});
// Use the exact same approach as add-task.js
const searchOptions = {
includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [
{ name: 'title', weight: 1.5 }, // Title is most important
{ name: 'description', weight: 2 }, // Description is very important
{ name: 'details', weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things
{ name: 'dependencyTitles', weight: 0.5 }
],
// Sort matches by score (lower is better)
shouldSort: true,
// Allow searching in nested properties
useExtendedSearch: true,
// Return up to 50 matches
limit: 50
};
// Create search index using Fuse.js
const fuse = new Fuse(searchableTasks, searchOptions);
// Extract significant words and phrases from the prompt (like add-task.js does)
const promptWords = query
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
// Use the user's prompt for fuzzy search
const fuzzyResults = fuse.search(query);
// Also search for each significant word to catch different aspects
const wordResults = [];
for (const word of promptWords) {
if (word.length > 5) {
// Only use significant words
const results = fuse.search(word);
if (results.length > 0) {
wordResults.push(...results);
} }
} }
} }
// Join all sections based on format // Merge and deduplicate results
const finalContext = this._joinContextSections(contextSections, format); const mergedResults = [...fuzzyResults];
if (includeTokenCounts) { // Add word results that aren't already in fuzzyResults
tokenBreakdown.total = this.countTokens(finalContext); for (const wordResult of wordResults) {
return { if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
context: finalContext, mergedResults.push(wordResult);
tokenBreakdown: tokenBreakdown }
};
} }
return finalContext; // Group search results by relevance
const highRelevance = mergedResults
.filter((result) => result.score < 0.25)
.map((result) => result.item);
const mediumRelevance = mergedResults
.filter((result) => result.score >= 0.25 && result.score < 0.4)
.map((result) => result.item);
// Get recent tasks (newest first)
const recentTasks = [...this.allTasks]
.sort((a, b) => b.id - a.id)
.slice(0, 5);
// Combine high relevance, medium relevance, and recent tasks
// Prioritize high relevance first
const allRelevantTasks = [...highRelevance];
// Add medium relevance if not already included
for (const task of mediumRelevance) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add recent tasks if not already included
for (const task of recentTasks) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Get top N results for context
const finalResults = allRelevantTasks.slice(0, maxResults);
return {
tasks: finalResults,
analysisData: {
highRelevance: highRelevance,
mediumRelevance: mediumRelevance,
recentTasks: recentTasks,
allRelevantTasks: allRelevantTasks
}
};
}
_buildDependencyContext(taskIds) {
const { allRelatedTaskIds, graphs, depthMap } =
this._buildDependencyGraphs(taskIds);
if (allRelatedTaskIds.size === 0) return '';
const dependentTasks = Array.from(allRelatedTaskIds)
.map((id) => this.allTasks.find((t) => t.id === id))
.filter(Boolean)
.sort((a, b) => (depthMap.get(a.id) || 0) - (depthMap.get(b.id) || 0));
const uniqueDetailedTasks = dependentTasks.slice(0, 8);
let context = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.`;
const directDeps = this.allTasks.filter((t) => taskIds.includes(t.id));
if (directDeps.length > 0) {
context += `\n\nDirect dependencies:\n${directDeps
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
}
const indirectDeps = dependentTasks.filter((t) => !taskIds.includes(t.id));
if (indirectDeps.length > 0) {
context += `\n\nIndirect dependencies (dependencies of dependencies):\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
if (indirectDeps.length > 5)
context += `\n- ... and ${
indirectDeps.length - 5
} more indirect dependencies`;
}
context += `\n\nDetailed information about dependencies:`;
for (const depTask of uniqueDetailedTasks) {
const isDirect = taskIds.includes(depTask.id)
? ' [DIRECT DEPENDENCY]'
: '';
context += `\n\n------ Task ${depTask.id}${isDirect}: ${depTask.title} ------\n`;
context += `Description: ${depTask.description}\n`;
if (depTask.dependencies?.length) {
context += `Dependencies: ${depTask.dependencies.join(', ')}\n`;
}
if (depTask.details) {
context += `Implementation Details: ${truncate(
depTask.details,
400
)}\n`;
}
}
if (graphs.length > 0) {
context += '\n\nDependency Chain Visualization:';
context += graphs
.map((graph) => this._formatDependencyChain(graph))
.join('');
}
return context;
}
_buildDependencyGraphs(taskIds) {
const visited = new Set();
const depthMap = new Map();
const graphs = [];
for (const id of taskIds) {
const graph = this._buildDependencyGraph(id, visited, depthMap);
if (graph) graphs.push(graph);
}
return { allRelatedTaskIds: visited, graphs, depthMap };
}
_buildDependencyGraph(taskId, visited, depthMap, depth = 0) {
if (visited.has(taskId) || depth > 5) return null; // Limit recursion depth
const task = this.allTasks.find((t) => t.id === taskId);
if (!task) return null;
visited.add(taskId);
if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
depthMap.set(taskId, depth);
}
const dependencies =
task.dependencies
?.map((depId) =>
this._buildDependencyGraph(depId, visited, depthMap, depth + 1)
)
.filter(Boolean) || [];
return { ...task, dependencies };
}
_formatDependencyChain(node, prefix = '', isLast = true, depth = 0) {
if (depth > 3) return '';
const connector = isLast ? '└── ' : '├── ';
let result = `${prefix}${connector}Task ${node.id}: ${node.title}`;
if (node.dependencies?.length) {
const childPrefix = prefix + (isLast ? ' ' : '│ ');
result += node.dependencies
.map((dep, index) =>
this._formatDependencyChain(
dep,
childPrefix,
index === node.dependencies.length - 1,
depth + 1
)
)
.join('');
}
return '\n' + result;
} }
/** /**
@@ -178,8 +424,7 @@ export class ContextGatherer {
*/ */
async _gatherTaskContext(taskIds, format, includeTokenCounts = false) { async _gatherTaskContext(taskIds, format, includeTokenCounts = false) {
try { try {
const tasksData = readJSON(this.tasksPath); if (!this.allTasks || this.allTasks.length === 0) {
if (!tasksData || !tasksData.tasks) {
return { context: null, breakdown: [] }; return { context: null, breakdown: [] };
} }
@@ -192,7 +437,7 @@ export class ContextGatherer {
let itemInfo = null; let itemInfo = null;
if (parsed.type === 'task') { if (parsed.type === 'task') {
const result = findTaskById(tasksData.tasks, parsed.taskId); const result = findTaskById(this.allTasks, parsed.taskId);
if (result.task) { if (result.task) {
formattedItem = this._formatTaskForContext(result.task, format); formattedItem = this._formatTaskForContext(result.task, format);
itemInfo = { itemInfo = {
@@ -204,7 +449,7 @@ export class ContextGatherer {
}; };
} }
} else if (parsed.type === 'subtask') { } else if (parsed.type === 'subtask') {
const parentResult = findTaskById(tasksData.tasks, parsed.parentId); const parentResult = findTaskById(this.allTasks, parsed.parentId);
if (parentResult.task && parentResult.task.subtasks) { if (parentResult.task && parentResult.task.subtasks) {
const subtask = parentResult.task.subtasks.find( const subtask = parentResult.task.subtasks.find(
(st) => st.id === parsed.subtaskId (st) => st.id === parsed.subtaskId
@@ -334,21 +579,16 @@ export class ContextGatherer {
: path.join(this.projectRoot, filePath); : path.join(this.projectRoot, filePath);
if (!fs.existsSync(fullPath)) { if (!fs.existsSync(fullPath)) {
console.warn(`Warning: File not found: ${filePath}`);
continue; continue;
} }
const stats = fs.statSync(fullPath); const stats = fs.statSync(fullPath);
if (!stats.isFile()) { if (!stats.isFile()) {
console.warn(`Warning: Path is not a file: ${filePath}`);
continue; continue;
} }
// Check file size (limit to 50KB for context) // Check file size (limit to 50KB for context)
if (stats.size > 50 * 1024) { if (stats.size > 50 * 1024) {
console.warn(
`Warning: File too large, skipping: ${filePath} (${Math.round(stats.size / 1024)}KB)`
);
continue; continue;
} }