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**
- **Purpose**: Contains core functions for task data manipulation (CRUD), AI interactions, and related logic.
- **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.
- **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.
- 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`).
- **[`dependency-manager.js`](mdc:scripts/modules/dependency-manager.js): Dependency Management**
- **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**
- **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**
- **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)):
- Reads and merges `.taskmasterconfig` with defaults.
- 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`).
- **[`utils.js`](mdc:scripts/modules/utils.js): Core Utility Functions**
@@ -62,6 +65,8 @@ alwaysApply: false
- Task utils (`findTaskById`), Dependency utils (`findCycles`).
- API Key Resolution (`resolveEnvVariable`).
- 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**
- **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.
- 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/`.
- **Tagged Task Lists**: MCP tools need updating to handle tagged format (Part 2 implementation).
- Manages MCP caching and response formatting.
- **[`init.js`](mdc:scripts/init.js): Project Initialization Logic**
- **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)**:
- **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.
- **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.
- **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/*`) -> **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.
## 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
- **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
- **Tagged System Tests**: Test migration, tag resolution, and multi-context functionality
- **Module Design for Testability**:
- **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
- **Callback Isolation**: Callbacks are defined as separate functions for easier testing
- **Stateless Design**: Modules avoid maintaining internal state where possible
- **Tag Resolution Testing**: Test both tagged and legacy format handling
- **Mock Integration Patterns**:
- **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
- **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
- **Tagged Data Mocking**: Test both legacy and tagged task data structures
- **Testing Flow**:
- 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
- Tests call the functions under test and verify behavior
- Mocks are reset between test cases to maintain isolation
- Tagged system behavior is tested for both migration and normal operation
- **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
- **Test Isolation**: Each component can be tested without affecting others
- **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.
- **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.
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.
- Return `{ success: true/false, data/error, fromCache: boolean }`.
- 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.
@@ -275,12 +314,8 @@ The `initialize_project` command provides a way to set up a new Task Master proj
- **MCP Tool**: `initialize_project`
- **Functionality**:
- Creates necessary directories and files for a new project
- Sets up `tasks.json` 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
- Sets up `tasks.json` with tagged structure and initial task files
- Configures project metadata (name, description, version)
- Initializes state.json for tag system
- Handles shell alias creation if requested
- 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.
- 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.
- **Note**: MCP tools will be updated for tagged task lists support in Part 2 of the implementation.
2. **`task-master` CLI (For Users & Fallback)**:
- 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 ...`.
- 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.
- **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
- 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
- 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
@@ -107,9 +141,10 @@ Taskmaster configuration is managed through two main mechanisms:
1. **`.taskmaster/config.json` File (Primary):**
* 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.
* **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.
* **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`):**
* 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`.
* 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.
**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.

View File

@@ -3,7 +3,6 @@ description: Git workflow integrated with Task Master for feature development an
globs: "**/*"
alwaysApply: true
---
# Git Workflow with Task Master Integration
## **Branch Strategy**
@@ -27,6 +26,26 @@ fix-database-issue
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**
```mermaid
@@ -34,7 +53,7 @@ flowchart TD
A[Start: On main branch] --> B[Pull latest changes]
B --> C[Create task branch<br/>git checkout -b task-XXX]
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]
F --> G[Set subtask: in-progress]
@@ -79,13 +98,13 @@ git branch # Verify you're on main
# 3. Create task-specific branch
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`
```
### **Phase 2: Task Analysis & Planning**
```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: 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**
```bash
# Task management
# Task management (uses current tag context automatically)
task-master show <id> # Get task/subtask details
task-master next # Find next task to work on
task-master set-status --id=<id> --status=<status>
@@ -302,16 +321,17 @@ Mention any dependent tasks or follow-up work needed.
## **Conflict Resolution**
### **Task Conflicts**
### **Task Conflicts with Tagged System**
```bash
# If multiple people work on overlapping tasks:
# 1. Use Task Master's move functionality to reorganize
task-master move --from=5 --to=25 # Move conflicting task
# With tagged task lists, merge conflicts are significantly reduced:
# 1. Different branches can use different tag contexts
# 2. Tasks in separate tags are completely isolated
# 3. Use Task Master's move functionality to reorganize if needed
# 2. Update task dependencies
task-master add-dependency --id=6 --depends-on=5
# 3. Coordinate through PR comments and task updates
# Future (Part 2): Enhanced git integration
# - Automatic tag creation based on branch names
# - Branch-tag mapping for seamless context switching
# - Git hooks for automated tag management
```
### **Code Conflicts**
@@ -359,6 +379,23 @@ git push origin --delete task-<id>
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:**

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.
- **[`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.
- **[`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.
- **[`dependencies.mdc`](mdc:.cursor/rules/dependencies.mdc)**: Guidelines for managing task dependencies and relationships.
- **[`dev_workflow.mdc`](mdc:.cursor/rules/dev_workflow.mdc)**: Guide for using Task Master to manage task-driven development workflows.
- **[`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 with tagged task lists support.
- **[`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.
- **[`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.
- **[`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)**: Comprehensive reference for Taskmaster MCP tools and CLI commands.
- **[`tasks.mdc`](mdc:.cursor/rules/tasks.mdc)**: Guidelines for implementing task management operations.
- **[`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 with tagged task lists system support.
- **[`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.
- **[`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.

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`.
**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

View File

@@ -3,9 +3,19 @@ description: Guidelines for implementing task management operations
globs: scripts/modules/task-manager.js
alwaysApply: false
---
# 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
- **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**:
- ✅ DO: Use consistent properties across subtasks
- ✅ DO: Maintain simple numeric IDs within parent tasks
@@ -48,53 +77,56 @@ alwaysApply: false
## Task Creation and Parsing
- **ID Management**:
- ✅ DO: Assign unique sequential IDs to tasks
- ✅ DO: Calculate the next ID based on existing tasks
- ❌ DON'T: Hardcode or reuse IDs
- ✅ DO: Assign unique sequential IDs to tasks within each tag context
- ✅ DO: Calculate the next ID based on existing tasks in the current tag
- ❌ DON'T: Hardcode or reuse IDs within the same tag
```javascript
// ✅ DO: Calculate the next available ID
const highestId = Math.max(...data.tasks.map(t => t.id));
// ✅ DO: Calculate the next available ID within the current tag
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;
```
- **PRD Parsing**:
- ✅ 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: Validate and clean up AI-generated tasks
```javascript
// ✅ DO: Validate AI responses
try {
// Parse the JSON response
taskData = JSON.parse(jsonContent);
// Check that we have the required fields
if (!taskData.title || !taskData.description) {
throw new Error("Missing required fields in the generated task");
}
} catch (error) {
log('error', "Failed to parse AI's response as valid task JSON:", error);
process.exit(1);
}
// ✅ DO: Parse into current tag context
const tasksData = readJSON(tasksPath) || {};
const currentTag = getCurrentTag() || 'master';
// Parse tasks and add to current tag
const newTasks = await parseTasksFromPRD(prdContent);
setTasksForTag(tasksData, currentTag, newTasks);
writeJSON(tasksPath, tasksData);
```
## Task Updates and Modifications
- **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: Consider subtask status when updating parent tasks
```javascript
// ✅ DO: Handle status updates for both tasks and subtasks
// ✅ DO: Handle status updates within tagged context
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")
if (taskIdInput.includes('.')) {
const [parentId, subtaskId] = taskIdInput.split('.').map(id => parseInt(id, 10));
// 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);
// Update subtask status
@@ -109,7 +141,7 @@ alwaysApply: false
}
} else {
// 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;
// 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**:
- ✅ 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: Ensure proper IDs for newly created subtasks
```javascript
// ✅ DO: Generate appropriate subtasks based on complexity
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
if (taskAnalysis) {
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}`);
}
}
// Generate subtasks and save back
// ... subtask generation logic ...
setTasksForTag(tasksData, currentTag, tasks);
writeJSON(tasksPath, tasksData);
```
## Task File Generation
@@ -155,67 +200,65 @@ alwaysApply: false
// Format dependencies with their status
if (task.dependencies && task.dependencies.length > 0) {
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, data.tasks)}\n`;
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, tasks)}\n`;
} else {
content += '# Dependencies: None\n';
}
```
- **Subtask Inclusion**:
- ✅ DO: Include subtasks in parent task files
- ✅ DO: Use consistent indentation for subtask sections
- DO: Display subtask dependencies with proper formatting
- **Tagged Context Awareness**:
- ✅ DO: Generate task files from current tag context
- ✅ DO: Include tag information in generated files
- DON'T: Mix tasks from different tags in file generation
```javascript
// ✅ DO: Format subtasks correctly in task files
if (task.subtasks && task.subtasks.length > 0) {
content += '\n# Subtasks:\n';
// ✅ DO: Generate files for current tag context
async function generateTaskFiles(tasksPath, outputDir) {
const tasksData = readJSON(tasksPath);
const currentTag = getCurrentTag() || 'master';
const tasks = getTasksForTag(tasksData, currentTag);
task.subtasks.forEach(subtask => {
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
// Format subtask dependencies
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';
});
// Add tag context to file header
let content = `# Tag Context: ${currentTag}\n`;
content += `# Task ID: ${task.id}\n`;
// ... rest of file generation
}
```
## Task Listing and Display
- **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: Use consistent table formats
```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
const filteredTasks = statusFilter
? data.tasks.filter(task =>
? tasks.filter(task =>
task.status && task.status.toLowerCase() === statusFilter.toLowerCase())
: data.tasks;
: tasks;
```
- **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: Use visual progress indicators
```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
const totalTasks = data.tasks.length;
const completedTasks = data.tasks.filter(task =>
const totalTasks = tasks.length;
const completedTasks = tasks.filter(task =>
task.status === 'done' || task.status === 'completed').length;
const completionPercentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
@@ -223,7 +266,7 @@ alwaysApply: false
let totalSubtasks = 0;
let completedSubtasks = 0;
data.tasks.forEach(task => {
tasks.forEach(task => {
if (task.subtasks && task.subtasks.length > 0) {
totalSubtasks += task.subtasks.length;
completedSubtasks += task.subtasks.filter(st =>
@@ -232,99 +275,52 @@ alwaysApply: false
});
```
## Complexity Analysis
## Migration and Compatibility
- **Scoring System**:
- ✅ DO: Use AI to analyze task complexity
- ✅ DO: Include complexity scores (1-10)
- ✅ DO: Generate specific expansion recommendations
- **Silent Migration Handling**:
- ✅ DO: Implement silent migration in `readJSON()` function
- ✅ DO: Detect legacy format and convert automatically
- ✅ DO: Preserve all existing task data during migration
```javascript
// ✅ DO: Handle complexity analysis properly
const report = {
meta: {
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...');
// ✅ DO: Handle silent migration (implemented in utils.js)
function readJSON(filepath) {
let data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
// Create a map of task IDs to complexity scores
const complexityMap = new Map();
complexityReport.complexityAnalysis.forEach(analysis => {
complexityMap.set(analysis.taskId, analysis.complexityScore);
});
// Silent migration for tasks.json files
if (data.tasks && Array.isArray(data.tasks) && !data.master && isTasksFile) {
const migratedData = {
master: {
tasks: data.tasks
}
};
writeJSON(filepath, migratedData);
data = migratedData;
}
// Sort tasks by complexity score (high to low)
tasksToExpand.sort((a, b) => {
const scoreA = complexityMap.get(a.id) || 0;
const scoreB = complexityMap.get(b.id) || 0;
return scoreB - scoreA;
});
return data;
}
```
## Next Task Selection
- **Eligibility Criteria**:
- DO: Consider dependencies when finding next tasks
- ✅ DO: Prioritize by task priority and dependency count
- ✅ DO: Skip completed tasks
- **Tag Resolution**:
- ✅ DO: Use tag resolution functions to maintain backward compatibility
- ✅ DO: Return legacy format to core functions
- DON'T: Expose tagged structure to existing core logic
```javascript
// ✅ DO: Use proper task prioritization logic
function findNextTask(tasks) {
// Get all completed task IDs
const completedTaskIds = new Set(
tasks
.filter(t => t.status === 'done' || t.status === 'completed')
.map(t => t.id)
);
// ✅ DO: Use tag resolution layer
function getTasksForTag(data, tagName) {
if (data.tasks && Array.isArray(data.tasks)) {
// Legacy format - return as-is
return data.tasks;
}
// Filter for pending tasks whose dependencies are all satisfied
const eligibleTasks = tasks.filter(task =>
(task.status === 'pending' || task.status === 'in-progress') &&
task.dependencies &&
task.dependencies.every(depId => completedTaskIds.has(depId))
);
if (data[tagName] && data[tagName].tasks) {
// Tagged format - return tasks for specified tag
return data[tagName].tasks;
}
// Sort by priority, dependency count, and ID
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;
return [];
}
```

View File

@@ -1,6 +1,6 @@
---
description: Guidelines for implementing utility functions
globs: scripts/modules/utils.js, mcp-server/src/**/*
description:
globs:
alwaysApply: false
---
# Utility Function Guidelines
@@ -600,4 +600,578 @@ export {
- ✅ DO: Sort discovered task IDs numerically for better readability
- ❌ 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"
},
"tags": {
"autoSwitchOnBranch": false,
"gitIntegration": {
"enabled": false,
"autoSwitchTagWithBranch": false
}
"enabledGitworkflow": 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.
</info added on 2025-05-25T06:29:01.194Z>
## 5. Direct Function Implementation [pending]
## 5. Direct Function Implementation [done]
### Dependencies: 98.4
### Description: Create the MCP direct function wrapper in mcp-server/src/core/direct-functions/ following the add-task pattern
### 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
- Export and register in task-master-core.js
## 6. MCP Tool Implementation [pending]
## 6. MCP Tool Implementation [done]
### Dependencies: 98.5
### Description: Create the MCP tool in mcp-server/src/tools/ following the add-task tool pattern
### Details:

View File

@@ -115,7 +115,7 @@ Ensure all features work as intended and meet quality standards, with specific f
### Details:
## 14. Create State Management Utilities [pending]
## 14. Create State Management Utilities [done]
### 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
### 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.
</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,
"defaultSubtasks": 5,
"defaultPriority": "medium",
"defaultTag": "master",
"projectName": "Your Project Name",
"ollamaBaseURL": "http://localhost:11434/api",
"azureBaseURL": "https://your-endpoint.azure.com/",
"vertexProjectId": "your-gcp-project-id",
"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.
## 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)
```

View File

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

View File

@@ -91,7 +91,13 @@ export function registerListTasksTool(server) {
log.info(
`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) {
log.error(`Error getting tasks: ${error.message}`);
return createErrorResponse(error.message);

View File

@@ -8,6 +8,7 @@ import path from 'path';
import fs from 'fs';
import { contextManager } from '../core/context-manager.js'; // Import the singleton
import { fileURLToPath } from 'url';
import { getCurrentTag } from '../../../scripts/modules/utils.js';
// Import path utilities to ensure consistent path resolution
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
* @param {string|undefined} projectRootRaw - Raw project root from arguments
@@ -242,21 +301,26 @@ function getProjectRootFromSession(session, log) {
* @param {Object} log - Logger object
* @param {string} errorPrefix - Prefix for error messages
* @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
*/
async function handleApiResult(
result,
log,
errorPrefix = 'API error',
processFunction = processMCPResponseData
processFunction = processMCPResponseData,
projectRoot = null
) {
// Get version info for every response
const versionInfo = getVersionInfo();
// Get tag info if project root is provided
const tagInfo = projectRoot ? getTagInfo(projectRoot, log) : null;
if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
log.error(`${errorPrefix}: ${errorMsg}`);
return createErrorResponse(errorMsg, versionInfo);
return createErrorResponse(errorMsg, versionInfo, tagInfo);
}
// Process the result data if needed
@@ -266,12 +330,17 @@ async function handleApiResult(
log.info('Successfully completed operation');
// Create the response payload including version info
// Create the response payload including version info and tag info
const responsePayload = {
data: processedData,
version: versionInfo
};
// Add tag information if available
if (tagInfo) {
responsePayload.tag = tagInfo;
}
return createContentResponse(responsePayload);
}
@@ -496,21 +565,30 @@ function createContentResponse(content) {
* Creates error response for tools
* @param {string} errorMessage - Error message to include in response
* @param {Object} [versionInfo] - Optional version information object
* @param {Object} [tagInfo] - Optional tag information object
* @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
if (!versionInfo) {
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 {
content: [
{
type: 'text',
text: `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`
text: responseText
}
],
isError: true
@@ -704,6 +782,7 @@ function withNormalizedProjectRoot(executeFn) {
export {
getProjectRoot,
getProjectRootFromSession,
getTagInfo,
handleApiResult,
executeTaskMasterCommand,
getCachedOrExecute,

24358
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,12 @@ import {
getVertexProjectId,
getVertexLocation
} from './config-manager.js';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
import {
log,
findProjectRoot,
resolveEnvVariable,
getCurrentTag
} from './utils.js';
// Import provider classes
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 ---
const MAX_RETRIES = 2;
const INITIAL_RETRY_DELAY_MS = 1000;
@@ -246,7 +310,7 @@ async function _attemptProviderCallWithRetries(
if (isRetryableError(error) && retries < MAX_RETRIES) {
retries++;
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1);
const delay = INITIAL_RETRY_DELAY_MS * 2 ** (retries - 1);
log(
'info',
`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.';
for (const currentRole of sequence) {
let providerName,
modelId,
apiKey,
roleParams,
provider,
baseURL,
providerResponse,
telemetryData = null;
let providerName;
let modelId;
let apiKey;
let roleParams;
let provider;
let baseURL;
let providerResponse;
let telemetryData = null;
try {
log('info', `New AI service call with role: ${currentRole}`);
@@ -555,9 +619,13 @@ async function _unifiedServiceRunner(serviceType, params) {
finalMainResult = providerResponse;
}
// Get tag information for the response
const tagInfo = _getTagInfo(effectiveProjectRoot);
return {
mainResult: finalMainResult,
telemetryData: telemetryData
telemetryData: telemetryData,
tagInfo: tagInfo
};
} catch (error) {
const cleanMessage = _extractErrorMessage(error);

View File

@@ -12,12 +12,14 @@ import {
stopLoadingIndicator,
succeedLoadingIndicator,
failLoadingIndicator,
displayAiUsageSummary
displayAiUsageSummary,
displayContextAnalysis
} from '../ui.js';
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import ContextGatherer from '../utils/contextGatherer.js';
// Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({
@@ -199,7 +201,9 @@ async function addTask(
const invalidDeps = dependencies.filter((depId) => {
// Ensure depId is parsed as a number for comparison
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) {
@@ -262,561 +266,27 @@ async function addTask(
// --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
// Create context string for task creation prompt
let contextTasks = '';
// Create a dependency map for better understanding of the task relationships
const taskMap = {};
data.tasks.forEach((t) => {
// 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
};
// --- Use the new ContextGatherer ---
const contextGatherer = new ContextGatherer(projectRoot);
const gatherResult = await contextGatherer.gather({
semanticQuery: prompt,
dependencyTasks: numericDependencies,
format: 'research'
});
// CLI-only feedback for the dependency analysis
if (outputFormat === 'text') {
console.log(
boxen(chalk.cyan.bold('Task Context Analysis'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0 },
borderColor: 'cyan',
borderStyle: 'round'
})
);
const gatheredContext = gatherResult.context;
const analysisData = gatherResult.analysisData;
// Display context analysis if not in silent mode
if (outputFormat === 'text' && analysisData) {
displayContextAnalysis(analysisData, prompt, gatheredContext.length);
}
// 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') {
console.log(
boxen(
chalk.white.bold('AI Task Generation') +
`\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
)}`,
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}`,
{
padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
@@ -825,7 +295,6 @@ async function addTask(
}
)
);
console.log(); // Add spacing
}
// System Prompt - Enhanced for dependency awareness
@@ -866,8 +335,7 @@ async function addTask(
// 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.
${contextTasks}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
${gatheredContext}
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) {
const allValidDeps = taskData.dependencies.every((depId) => {
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) {
@@ -985,7 +455,9 @@ async function addTask(
);
newTask.dependencies = taskData.dependencies.filter((depId) => {
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';
case 'low':
return 'gray';
case 'medium':
default:
return 'yellow';
}

View File

@@ -18,19 +18,32 @@ import {
COMPLEXITY_REPORT_FILE,
LEGACY_TASKS_FILE
} 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.
* (Moved from ai-services.js and simplified)
* @param {Object} tasksData - The tasks data object.
* @param {string} [gatheredContext] - The gathered context for the analysis.
* @returns {string} The generated prompt.
*/
function generateInternalComplexityAnalysisPrompt(tasksData) {
function generateInternalComplexityAnalysisPrompt(
tasksData,
gatheredContext = ''
) {
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:
${tasksString}
${tasksString}`;
if (gatheredContext) {
prompt += `\n\n# Project Context\n\n${gatheredContext}`;
}
prompt += `
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.`;
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;
reportLog(
`Found ${originalTaskCount} total tasks in the task file.`,
@@ -226,7 +275,7 @@ async function analyzeTaskComplexity(options, context = {}) {
// Check for existing report before doing analysis
let existingReport = null;
let existingAnalysisMap = new Map(); // For quick lookups by task ID
const existingAnalysisMap = new Map(); // For quick lookups by task ID
try {
if (fs.existsSync(outputPath)) {
existingReport = readJSON(outputPath);
@@ -342,7 +391,10 @@ async function analyzeTaskComplexity(options, context = {}) {
}
// Continue with regular analysis path
const prompt = generateInternalComplexityAnalysisPrompt(tasksData);
const prompt = generateInternalComplexityAnalysisPrompt(
tasksData,
gatheredContext
);
const systemPrompt =
'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 {
startLoadingIndicator,
stopLoadingIndicator,
@@ -32,9 +32,14 @@ async function expandAllTasks(
context = {},
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 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
const logger =
mcpLog ||
@@ -69,7 +74,7 @@ async function expandAllTasks(
try {
logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks) {
throw new Error(`Invalid tasks data in ${tasksPath}`);
}
@@ -119,7 +124,7 @@ async function expandAllTasks(
numSubtasks,
useResearch,
additionalContext,
context, // Pass the whole context object { session, mcpLog }
{ ...context, projectRoot }, // Pass the whole context object with projectRoot
force
);
expandedCount++;

View File

@@ -15,6 +15,9 @@ import { generateTextService } from '../ai-services-unified.js';
import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.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) ---
const subtaskSchema = z
@@ -285,9 +288,9 @@ function parseSubtasksFromText(
const patternStartIndex = jsonToParse.indexOf(targetPattern);
if (patternStartIndex !== -1) {
let openBraces = 0;
let firstBraceFound = false;
let extractedJsonBlock = '';
const openBraces = 0;
const firstBraceFound = false;
const extractedJsonBlock = '';
// ... (loop for brace counting as before) ...
// ... (if successful, jsonToParse = extractedJsonBlock) ...
// ... (if that fails, fallbacks as before) ...
@@ -349,7 +352,8 @@ function parseSubtasksFromText(
? rawSubtask.dependencies
.map((dep) => (typeof dep === 'string' ? parseInt(dep, 10) : dep))
.filter(
(depId) => !isNaN(depId) && depId >= startId && depId < currentId
(depId) =>
!Number.isNaN(depId) && depId >= startId && depId < currentId
)
: [],
status: 'pending'
@@ -418,7 +422,9 @@ async function expandTask(
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
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
const logger = mcpLog || {
@@ -436,7 +442,7 @@ async function expandTask(
try {
// --- Task Loading/Filtering (Unchanged) ---
logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks)
throw new Error(`Invalid tasks data in ${tasksPath}`);
const taskIndex = data.tasks.findIndex(
@@ -458,6 +464,35 @@ async function expandTask(
}
// --- 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 ---
let finalSubtaskCount;
let promptContent = '';
@@ -498,7 +533,7 @@ async function expandTask(
// Determine final subtask count
const explicitNumSubtasks = parseInt(numSubtasks, 10);
if (!isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks > 0) {
finalSubtaskCount = explicitNumSubtasks;
logger.info(
`Using explicitly provided subtask count: ${finalSubtaskCount}`
@@ -512,7 +547,7 @@ async function expandTask(
finalSubtaskCount = getDefaultSubtasks(session);
logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
}
if (isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount <= 0) {
logger.warn(
`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
);
@@ -528,6 +563,9 @@ async function expandTask(
// Append additional context and reasoning
promptContent += `\n\n${additionalContext}`.trim();
promptContent += `${complexityReasoningContext}`.trim();
if (gatheredContext) {
promptContent += `\n\n# Project Context\n\n${gatheredContext}`;
}
// --- 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.`;
@@ -537,8 +575,13 @@ async function expandTask(
// --- End Simplified System Prompt ---
} else {
// Use standard prompt generation
const combinedAdditionalContext =
let combinedAdditionalContext =
`${additionalContext}${complexityReasoningContext}`.trim();
if (gatheredContext) {
combinedAdditionalContext =
`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
}
if (useResearch) {
promptContent = generateResearchUserPrompt(
task,

View File

@@ -11,7 +11,12 @@ import { highlight } from 'cli-highlight';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.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 {
displayAiUsageSummary,
startLoadingIndicator,
@@ -579,42 +584,6 @@ function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
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
* @param {Object} originalOptions - Original research options

View File

@@ -15,11 +15,15 @@ import {
readJSON,
writeJSON,
truncate,
isSilentMode
isSilentMode,
findProjectRoot,
flattenTasksWithSubtasks
} from '../utils.js';
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.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.
@@ -42,7 +46,7 @@ async function updateSubtaskById(
context = {},
outputFormat = context.mcpLog ? 'json' : 'text'
) {
const { session, mcpLog, projectRoot } = context;
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog;
@@ -81,7 +85,12 @@ async function updateSubtaskById(
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) {
throw new Error(
`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);
if (
isNaN(parentId) ||
Number.isNaN(parentId) ||
parentId <= 0 ||
isNaN(subtaskIdNum) ||
Number.isNaN(subtaskIdNum) ||
subtaskIdNum <= 0
) {
throw new Error(
@@ -125,6 +134,35 @@ async function updateSubtaskById(
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') {
const table = new Table({
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...").`;
// 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';
report('info', `Using AI text service with role: ${role}`);

View File

@@ -10,7 +10,9 @@ import {
readJSON,
writeJSON,
truncate,
isSilentMode
isSilentMode,
flattenTasksWithSubtasks,
findProjectRoot
} from '../utils.js';
import {
@@ -26,6 +28,8 @@ import {
isApiKeySet // Keep this check
} from '../config-manager.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
const updatedTaskSchema = z
@@ -216,7 +220,7 @@ async function updateTaskById(
context = {},
outputFormat = 'text'
) {
const { session, mcpLog, projectRoot } = context;
const { session, mcpLog, projectRoot: providedProjectRoot } = context;
const logFn = mcpLog || consoleLog;
const isMCP = !!mcpLog;
@@ -255,8 +259,14 @@ async function updateTaskById(
throw new Error(`Tasks file not found: ${tasksPath}`);
// --- 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) ---
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}.`);
const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
@@ -293,6 +303,35 @@ async function updateTaskById(
}
// --- 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) ---
if (outputFormat === 'text') {
// 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.`;
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 ---
let loadingIndicator = null;

View File

@@ -23,6 +23,9 @@ import { getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { generateTextService } from '../ai-services-unified.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
const updatedTaskSchema = z
@@ -228,7 +231,7 @@ async function updateTasks(
context = {},
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
const logFn = mcpLog || consoleLog;
// Flag to easily check which logger type we have
@@ -246,8 +249,14 @@ async function updateTasks(
`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) ---
const data = readJSON(tasksPath);
const data = readJSON(tasksPath, projectRoot);
if (!data || !data.tasks)
throw new Error(`No valid tasks found in ${tasksPath}`);
const tasksToUpdate = data.tasks.filter(
@@ -263,6 +272,38 @@ async function updateTasks(
}
// --- 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) ---
if (outputFormat === 'text') {
// 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
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 ---
// --- 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 {
displayBanner,
@@ -2567,5 +2659,6 @@ export {
succeedLoadingIndicator,
failLoadingIndicator,
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
* @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
let isDebug = false; // Default fallback
try {
@@ -211,30 +212,38 @@ function readJSON(filepath) {
}
try {
if (!fs.existsSync(filepath)) {
if (isDebug) {
log('debug', `File not found: ${filepath}`);
}
return null;
}
const rawData = fs.readFileSync(filepath, 'utf8');
let data = JSON.parse(rawData);
// Silent migration for tasks.json files: Transform old format to tagged format
// 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';
// Check if this is legacy tasks.json format that needs migration
if (
data &&
data.tasks &&
Array.isArray(data.tasks) &&
!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 = {
master: {
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
try {
writeJSON(filepath, migratedData);
@@ -282,28 +291,43 @@ function readJSON(filepath) {
);
if (hasTaggedFormat) {
// This is tagged format - resolve which tag to use
// Derive project root from filepath to get correct tag context
const projectRoot =
findProjectRoot(path.dirname(filepath)) || path.dirname(filepath);
const resolvedTag = resolveTag({ projectRoot });
// Default to master tag if anything goes wrong
let resolvedTag = 'master';
// 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) {
// Return data in old format so existing code continues to work
data = {
tag: resolvedTag,
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 {
// Tag doesn't exist, create empty tasks array and log warning
if (isDebug) {
log(
'warn',
`Tag "${resolvedTag}" not found in tasks file, using empty tasks array`
);
}
data = { tasks: [], tag: resolvedTag, _rawTaggedData: data };
// No valid tags found, return empty
data = { tasks: [], tag: 'master', _rawTaggedData: data };
}
}
}
@@ -362,45 +386,41 @@ function performCompleteTagMigration(tasksJsonPath) {
*/
function migrateConfigJson(configPath) {
try {
const configData = readJSON(configPath);
if (!configData) return;
const rawConfig = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(rawConfig);
if (!config) return;
let needsUpdate = false;
let modified = false;
// Add defaultTag to global section if missing
if (!configData.global) {
configData.global = {};
// Add global.defaultTag if missing
if (!config.global) {
config.global = {};
}
if (!configData.global.defaultTag) {
configData.global.defaultTag = 'master';
needsUpdate = true;
if (!config.global.defaultTag) {
config.global.defaultTag = 'master';
modified = true;
}
// Add tags section if missing
if (!configData.tags) {
configData.tags = {
autoSwitchOnBranch: false,
gitIntegration: {
enabled: false,
autoSwitchTagWithBranch: false
}
if (!config.tags) {
config.tags = {
enabledGitworkflow: false,
autoSwitchTagWithBranch: false
};
needsUpdate = true;
modified = true;
}
if (needsUpdate) {
writeJSON(configPath, configData);
if (getDebugFlag()) {
log(
'debug',
`Migrated config.json with tagged task system configuration`
if (modified) {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log(
'[DEBUG] Updated config.json with tagged task system settings'
);
}
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error migrating config.json: ${error.message}`);
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error migrating config.json: ${error.message}`);
}
}
}
@@ -418,13 +438,13 @@ function createStateJson(statePath) {
migrationNoticeShown: false
};
writeJSON(statePath, initialState);
if (getDebugFlag()) {
log('debug', `Created initial state.json for tagged task system`);
fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log('[DEBUG] Created initial state.json for tagged task system');
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error creating state.json: ${error.message}`);
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error creating state.json: ${error.message}`);
}
}
}
@@ -445,16 +465,27 @@ function markMigrationForNotice(tasksJsonPath) {
createStateJson(statePath);
}
// Read and update state to mark migration occurred
const stateData = readJSON(statePath) || {};
if (stateData.migrationNoticeShown !== false) {
// Set to false to trigger notice display
stateData.migrationNoticeShown = false;
writeJSON(statePath, stateData);
// Read and update state to mark migration occurred using fs directly
try {
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState) || {};
if (stateData.migrationNoticeShown !== false) {
// 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) {
if (getDebugFlag()) {
log('warn', `Error marking migration for notice: ${error.message}`);
if (process.env.TASKMASTER_DEBUG === 'true') {
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
* @param {string} projectRoot - The project root directory
* @param {string} projectRoot - The project root directory (required)
* @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 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');
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) {
return stateData.currentTag;
}
@@ -915,11 +951,12 @@ function getCurrentTag(projectRoot = process.cwd()) {
// 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 {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
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) {
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 {string} [options.tag] - Explicit tag from --tag flag
* @param {string} [options.projectRoot] - Project root directory
* @param {string} options.projectRoot - The project root directory (required)
* @param {string} [options.tag] - Explicit tag to use
* @returns {string} The resolved tag name
*/
function resolveTag(options = {}) {
// Priority: explicit tag > current tag from state > defaultTag from config > 'master'
if (options.tag) {
return options.tag;
const { projectRoot, tag } = options;
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;
}
/**
* 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 {
LOG_LEVELS,
@@ -1022,5 +1102,6 @@ export {
performCompleteTagMigration,
migrateConfigJson,
createStateJson,
markMigrationForNotice
markMigrationForNotice,
flattenTasksWithSubtasks
};

View File

@@ -7,7 +7,13 @@
import fs from 'fs';
import path from 'path';
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;
@@ -17,7 +23,26 @@ const { encode } = pkg;
export class ContextGatherer {
constructor(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 {boolean} [options.includeProjectTree] - Include project file tree
* @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 = {}) {
const {
@@ -55,86 +83,304 @@ export class ContextGatherer {
customContext = '',
includeProjectTree = false,
format = 'research',
includeTokenCounts = false
semanticQuery,
maxSemanticResults = 10,
dependencyTasks = []
} = options;
const contextSections = [];
const tokenBreakdown = {
customContext: null,
tasks: [],
files: [],
projectTree: null,
total: 0
};
const finalTaskIds = new Set(tasks.map(String));
let analysisData = null;
// Add custom context first if provided
if (customContext && customContext.trim()) {
const formattedCustom = this._formatCustomContext(customContext, format);
contextSections.push(formattedCustom);
if (includeTokenCounts) {
tokenBreakdown.customContext = {
tokens: this.countTokens(formattedCustom),
characters: formattedCustom.length
};
}
// Semantic Search
if (semanticQuery && this.allTasks.length > 0) {
const semanticResults = this._performSemanticSearch(
semanticQuery,
maxSemanticResults
);
// Store the analysis data for UI display
analysisData = semanticResults.analysisData;
semanticResults.tasks.forEach((task) => {
finalTaskIds.add(String(task.id));
});
}
// Add task context
if (tasks.length > 0) {
// Dependency Graph Analysis
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(
tasks,
format,
includeTokenCounts
Array.from(finalTaskIds),
format
);
if (taskContextResult.context) {
contextSections.push(taskContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.tasks = taskContextResult.breakdown;
}
}
}
// Add file context
if (files.length > 0) {
const fileContextResult = await this._gatherFileContext(
files,
format,
includeTokenCounts
);
const fileContextResult = await this._gatherFileContext(files, format);
if (fileContextResult.context) {
contextSections.push(fileContextResult.context);
if (includeTokenCounts) {
tokenBreakdown.files = fileContextResult.breakdown;
}
}
}
// Add project tree context
if (includeProjectTree) {
const treeContextResult = await this._gatherProjectTreeContext(
format,
includeTokenCounts
);
const treeContextResult = await this._gatherProjectTreeContext(format);
if (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
const finalContext = this._joinContextSections(contextSections, format);
// Merge and deduplicate results
const mergedResults = [...fuzzyResults];
if (includeTokenCounts) {
tokenBreakdown.total = this.countTokens(finalContext);
return {
context: finalContext,
tokenBreakdown: tokenBreakdown
};
// 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);
}
}
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) {
try {
const tasksData = readJSON(this.tasksPath);
if (!tasksData || !tasksData.tasks) {
if (!this.allTasks || this.allTasks.length === 0) {
return { context: null, breakdown: [] };
}
@@ -192,7 +437,7 @@ export class ContextGatherer {
let itemInfo = null;
if (parsed.type === 'task') {
const result = findTaskById(tasksData.tasks, parsed.taskId);
const result = findTaskById(this.allTasks, parsed.taskId);
if (result.task) {
formattedItem = this._formatTaskForContext(result.task, format);
itemInfo = {
@@ -204,7 +449,7 @@ export class ContextGatherer {
};
}
} 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) {
const subtask = parentResult.task.subtasks.find(
(st) => st.id === parsed.subtaskId
@@ -334,21 +579,16 @@ export class ContextGatherer {
: path.join(this.projectRoot, filePath);
if (!fs.existsSync(fullPath)) {
console.warn(`Warning: File not found: ${filePath}`);
continue;
}
const stats = fs.statSync(fullPath);
if (!stats.isFile()) {
console.warn(`Warning: Path is not a file: ${filePath}`);
continue;
}
// Check file size (limit to 50KB for context)
if (stats.size > 50 * 1024) {
console.warn(
`Warning: File too large, skipping: ${filePath} (${Math.round(stats.size / 1024)}KB)`
);
continue;
}