Compare commits

..

8 Commits

Author SHA1 Message Date
Ralph Khreish
06b5a476b3 chore: bump gemini cli package 2025-08-28 19:11:06 +02:00
Ralph Khreish
3552f9424c chore: apply requested coderabbit changes 2025-08-28 18:51:05 +02:00
Ralph Khreish
c34d1417ca feat: add google cli integration and fix tests 2025-08-28 18:42:32 +02:00
Ralph Khreish
9f8cb08515 feat: add gemini-cli support for codebase context 2025-08-28 18:16:55 +02:00
Ralph Khreish
7fd751a5b8 chore: fix test 2025-08-28 18:16:55 +02:00
Ralph Khreish
6e5d44003d chore: format 2025-08-28 18:16:55 +02:00
Ralph Khreish
f746d84145 feat: fix CI and format + refactor 2025-08-28 18:16:55 +02:00
Ralph Khreish
e31500e9b0 feat: add support for claude code context
- code context for:
  - add-task
  - update-subtask
  - update-task
  - update
2025-08-28 18:16:27 +02:00
31 changed files with 231 additions and 652 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples

View File

@@ -1,15 +0,0 @@
{
"mode": "pre",
"tag": "rc",
"initialVersions": {
"task-master-ai": "0.25.1",
"docs": "0.0.1",
"extension": "0.24.1"
},
"changesets": [
"clarify-force-move-docs",
"curvy-moons-dig",
"strong-eagles-vanish",
"wet-candies-accept"
]
}

View File

@@ -0,0 +1,8 @@
---
"task-master-ai": patch
---
fix(claude-code): prevent crash/hang when the optional `@anthropic-ai/claude-code` SDK is missing by guarding `AbortError instanceof` checks and adding explicit SDK presence checks in `doGenerate`/`doStream`. Also bump the optional dependency to `^1.0.88` for improved export consistency.
Related to JSON truncation handling in #920; this change addresses a separate error-path crash reported in #1142.

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Temporarily disable streaming for improved model compatibility - will be re-enabled in upcoming release

View File

@@ -1,11 +0,0 @@
---
"task-master-ai": minor
---
Add configurable codebase analysis feature flag with multiple configuration sources
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
Priority order: .env > MCP session env > .taskmaster/config.json.
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.

View File

@@ -1,12 +0,0 @@
---
"task-master-ai": minor
---
feat(move): improve cross-tag move UX and safety
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
---

View File

@@ -1,48 +1,5 @@
# task-master-ai # task-master-ai
## 0.26.0-rc.0
### Minor Changes
- [#1163](https://github.com/eyaltoledano/claude-task-master/pull/1163) [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Gemini CLI provider with codebase-aware task generation
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - feat(move): improve cross-tag move UX and safety
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
***
- [#1162](https://github.com/eyaltoledano/claude-task-master/pull/1162) [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
Commands contextualised:
- add-task
- update-subtask
- update-task
- update
### Patch Changes
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
## 0.25.1
### Patch Changes
- [#1152](https://github.com/eyaltoledano/claude-task-master/pull/1152) [`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b) Thanks [@ben-vargas](https://github.com/ben-vargas)! - fix(claude-code): prevent crash/hang when the optional `@anthropic-ai/claude-code` SDK is missing by guarding `AbortError instanceof` checks and adding explicit SDK presence checks in `doGenerate`/`doStream`. Also bump the optional dependency to `^1.0.88` for improved export consistency.
Related to JSON truncation handling in #920; this change addresses a separate error-path crash reported in #1142.
- [#1151](https://github.com/eyaltoledano/claude-task-master/pull/1151) [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Temporarily disable streaming for improved model compatibility - will be re-enabled in upcoming release
## 0.25.0 ## 0.25.0
### Minor Changes ### Minor Changes

View File

@@ -1,19 +1,5 @@
# Change Log # Change Log
## 0.24.2-rc.0
### Patch Changes
- Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
- task-master-ai@0.26.0-rc.0
## 0.24.1
### Patch Changes
- Updated dependencies [[`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b), [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de)]:
- task-master-ai@0.25.1
## 0.24.0 ## 0.24.0
### Minor Changes ### Minor Changes

View File

@@ -3,7 +3,7 @@
"private": true, "private": true,
"displayName": "TaskMaster", "displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code", "description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.24.2-rc.0", "version": "0.24.0",
"publisher": "Hamster", "publisher": "Hamster",
"icon": "assets/icon.png", "icon": "assets/icon.png",
"engines": { "engines": {
@@ -239,7 +239,7 @@
"check-types": "tsc --noEmit" "check-types": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"task-master-ai": "0.26.0-rc.0" "task-master-ai": "0.25.0"
}, },
"devDependencies": { "devDependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -65,10 +65,11 @@ This removes the dependency relationships and moves only the specified task.
### Force Move ### Force Move
Note: Force moves are no longer supported. Instead, use one of these options: Force the move even with dependency conflicts:
- `--with-dependencies` — move dependents together ```bash
- `--ignore-dependencies` — break cross-tag dependencies task-master move --from=5 --from-tag=backlog --to-tag=in-progress --force
```
⚠️ **Warning**: This may break dependency relationships and should be used with caution. ⚠️ **Warning**: This may break dependency relationships and should be used with caution.
@@ -92,7 +93,7 @@ Resolution options:
2. Break dependencies: task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --ignore-dependencies 2. Break dependencies: task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --ignore-dependencies
3. Validate and fix dependencies: task-master validate-dependencies && task-master fix-dependencies 3. Validate and fix dependencies: task-master validate-dependencies && task-master fix-dependencies
4. Move dependencies first: task-master move --from=2,3 --from-tag=backlog --to-tag=in-progress 4. Move dependencies first: task-master move --from=2,3 --from-tag=backlog --to-tag=in-progress
5. After deciding, re-run the move with either --with-dependencies or --ignore-dependencies 5. Force move (may break dependencies): task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --force
``` ```
### Subtask Movement Restrictions ### Subtask Movement Restrictions
@@ -148,6 +149,7 @@ task-master fix-dependencies
- **`--with-dependencies`**: When you want to maintain task relationships - **`--with-dependencies`**: When you want to maintain task relationships
- **`--ignore-dependencies`**: When you want to break cross-tag dependencies - **`--ignore-dependencies`**: When you want to break cross-tag dependencies
- **`--force`**: Only when you understand the consequences
### 3. Organize by Context ### 3. Organize by Context
@@ -263,14 +265,9 @@ task-master move --from=5 --from-tag=backlog --to-tag=done --ignore-dependencies
### Scenario 3: Force Move ### Scenario 3: Force Move
Choose one of these options explicitly:
```bash ```bash
# Move together with dependencies # Force move despite conflicts
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies task-master move --from=5 --from-tag=backlog --to-tag=in-progress --force
# Or break dependencies
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies
``` ```
### Scenario 4: Moving Subtasks ### Scenario 4: Moving Subtasks

View File

@@ -189,17 +189,6 @@ export async function moveTaskCrossTagDirect(args, log, context = {}) {
'Verify task IDs exist: task-master list', 'Verify task IDs exist: task-master list',
'Check task details: task-master show <id>' 'Check task details: task-master show <id>'
]; ];
} else if (
error.code === 'TASK_ALREADY_EXISTS' ||
error.message?.includes('already exists in target tag')
) {
// Target tag has an ID collision
errorCode = 'TASK_ALREADY_EXISTS';
suggestions = [
'Choose a different target tag without conflicting IDs',
'Move a different set of IDs (avoid existing ones)',
'If needed, move within-tag to a new ID first, then cross-tag move'
];
} }
return { return {

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.25.1", "version": "0.25.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.25.1", "version": "0.25.0",
"license": "MIT WITH Commons-Clause", "license": "MIT WITH Commons-Clause",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
@@ -93,9 +93,9 @@
} }
}, },
"apps/extension": { "apps/extension": {
"version": "0.24.1", "version": "0.24.0",
"dependencies": { "dependencies": {
"task-master-ai": "0.25.1" "task-master-ai": "0.25.0"
}, },
"devDependencies": { "devDependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.26.0-rc.0", "version": "0.25.0",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.", "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",

View File

@@ -4103,7 +4103,12 @@ Examples:
' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies' ' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies'
) + ) +
'\n\n' + '\n\n' +
chalk.white(' # Force move (may break dependencies)') +
'\n' + '\n' +
chalk.white(
' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --force'
) +
'\n\n' +
chalk.yellow.bold('Best Practices:') + chalk.yellow.bold('Best Practices:') +
'\n' + '\n' +
chalk.white( chalk.white(
@@ -4114,6 +4119,10 @@ Examples:
' • Use --ignore-dependencies to break cross-tag dependencies' ' • Use --ignore-dependencies to break cross-tag dependencies'
) + ) +
'\n' + '\n' +
chalk.white(
' • Use --force only when you understand the consequences'
) +
'\n' +
chalk.white( chalk.white(
' • Check dependencies first: task-master validate-dependencies' ' • Check dependencies first: task-master validate-dependencies'
) + ) +
@@ -4175,12 +4184,6 @@ Examples:
console.log(chalk.green(`${result.message}`)); console.log(chalk.green(`${result.message}`));
// Print any tips returned from the move operation (e.g., after ignoring dependencies)
if (Array.isArray(result.tips) && result.tips.length > 0) {
console.log('\n' + chalk.yellow.bold('Next Steps:'));
result.tips.forEach((t) => console.log(chalk.white(`${t}`)));
}
// Check if source tag still contains tasks before regenerating files // Check if source tag still contains tasks before regenerating files
const tasksData = readJSON( const tasksData = readJSON(
taskMaster.getTasksPath(), taskMaster.getTasksPath(),
@@ -4447,27 +4450,6 @@ Examples:
await handleWithinTagMove(moveContext); await handleWithinTagMove(moveContext);
} }
} catch (error) { } catch (error) {
const errMsg = String(error && (error.message || error));
if (errMsg.includes('already exists in target tag')) {
console.error(chalk.red(`Error: ${errMsg}`));
console.log(
'\n' +
chalk.yellow.bold('Conflict: ID already exists in target tag') +
'\n' +
chalk.white(
' • Choose a different target tag without conflicting IDs'
) +
'\n' +
chalk.white(
' • Move a different set of IDs (avoid existing ones)'
) +
'\n' +
chalk.white(
' • If needed, move within-tag to a new ID first, then cross-tag move'
)
);
process.exit(1);
}
handleMoveError(error, moveContext); handleMoveError(error, moveContext);
} }
}); });

View File

@@ -72,8 +72,7 @@ const DEFAULTS = {
projectName: 'Task Master', projectName: 'Task Master',
ollamaBaseURL: 'http://localhost:11434/api', ollamaBaseURL: 'http://localhost:11434/api',
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com', bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
responseLanguage: 'English', responseLanguage: 'English'
enableCodebaseAnalysis: true
}, },
claudeCode: {} claudeCode: {}
}; };
@@ -429,56 +428,15 @@ function getResearchProvider(explicitRoot = null) {
} }
/** /**
* Check if codebase analysis feature flag is enabled across all sources * Check if a codebase analysis provider is being used (Claude Code or Gemini CLI)
* Priority: .env > MCP env > config.json
* @param {object|null} session - MCP session object (optional)
* @param {string|null} projectRoot - Project root path (optional)
* @returns {boolean} True if codebase analysis is enabled
*/
function isCodebaseAnalysisEnabled(session = null, projectRoot = null) {
// Priority 1: Environment variable
const envFlag = resolveEnvVariable(
'TASKMASTER_ENABLE_CODEBASE_ANALYSIS',
session,
projectRoot
);
if (envFlag !== null && envFlag !== undefined && envFlag !== '') {
return envFlag.toLowerCase() === 'true' || envFlag === '1';
}
// Priority 2: MCP session environment
if (session?.env?.TASKMASTER_ENABLE_CODEBASE_ANALYSIS) {
const mcpFlag = session.env.TASKMASTER_ENABLE_CODEBASE_ANALYSIS;
return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1';
}
// Priority 3: Configuration file
const globalConfig = getGlobalConfig(projectRoot);
return globalConfig.enableCodebaseAnalysis !== false; // Default to true
}
/**
* Check if codebase analysis is available and enabled
* @param {boolean} useResearch - Whether to check research provider or main provider * @param {boolean} useResearch - Whether to check research provider or main provider
* @param {string|null} projectRoot - Project root path (optional) * @param {string|null} projectRoot - Project root path (optional)
* @param {object|null} session - MCP session object (optional) * @returns {boolean} True if a codebase analysis provider is the current provider
* @returns {boolean} True if codebase analysis is available and enabled
*/ */
function hasCodebaseAnalysis( function hasCodebaseAnalysis(useResearch = false, projectRoot = null) {
useResearch = false,
projectRoot = null,
session = null
) {
// First check if the feature is enabled
if (!isCodebaseAnalysisEnabled(session, projectRoot)) {
return false;
}
// Then check if a codebase analysis provider is configured
const currentProvider = useResearch const currentProvider = useResearch
? getResearchProvider(projectRoot) ? getResearchProvider(projectRoot)
: getMainProvider(projectRoot); : getMainProvider(projectRoot);
return ( return (
currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE || currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI
@@ -600,11 +558,6 @@ function getResponseLanguage(explicitRoot = null) {
return getGlobalConfig(explicitRoot).responseLanguage; return getGlobalConfig(explicitRoot).responseLanguage;
} }
function getCodebaseAnalysisEnabled(explicitRoot = null) {
// Directly return value from config
return getGlobalConfig(explicitRoot).enableCodebaseAnalysis;
}
/** /**
* Gets model parameters (maxTokens, temperature) for a specific role, * Gets model parameters (maxTokens, temperature) for a specific role,
* considering model-specific overrides from supported-models.json. * considering model-specific overrides from supported-models.json.
@@ -1063,8 +1016,6 @@ export {
getAzureBaseURL, getAzureBaseURL,
getBedrockBaseURL, getBedrockBaseURL,
getResponseLanguage, getResponseLanguage,
getCodebaseAnalysisEnabled,
isCodebaseAnalysisEnabled,
getParametersForRole, getParametersForRole,
getUserId, getUserId,
// API Key Checkers (still relevant) // API Key Checkers (still relevant)

View File

@@ -426,11 +426,7 @@ async function addTask(
useResearch, useResearch,
priority: effectivePriority, priority: effectivePriority,
dependencies: numericDependencies, dependencies: numericDependencies,
hasCodebaseAnalysis: hasCodebaseAnalysis( hasCodebaseAnalysis: hasCodebaseAnalysis(useResearch, projectRoot),
useResearch,
projectRoot,
session
),
projectRoot: projectRoot projectRoot: projectRoot
} }
); );

View File

@@ -419,11 +419,7 @@ async function analyzeTaskComplexity(options, context = {}) {
tasks: tasksData.tasks, tasks: tasksData.tasks,
gatheredContext: gatheredContext || '', gatheredContext: gatheredContext || '',
useResearch: useResearch, useResearch: useResearch,
hasCodebaseAnalysis: hasCodebaseAnalysis( hasCodebaseAnalysis: hasCodebaseAnalysis(useResearch, projectRoot),
useResearch,
projectRoot,
session
),
projectRoot: projectRoot || '' projectRoot: projectRoot || ''
}; };

View File

@@ -458,8 +458,7 @@ async function expandTask(
// Check if a codebase analysis provider is being used // Check if a codebase analysis provider is being used
const hasCodebaseAnalysisCapability = hasCodebaseAnalysis( const hasCodebaseAnalysisCapability = hasCodebaseAnalysis(
useResearch, useResearch,
projectRoot, projectRoot
session
); );
// Combine all context sources into a single additionalContext parameter // Combine all context sources into a single additionalContext parameter

View File

@@ -61,43 +61,6 @@ const MOVE_ERROR_CODES = {
INVALID_TARGET_TAG: 'INVALID_TARGET_TAG' INVALID_TARGET_TAG: 'INVALID_TARGET_TAG'
}; };
/**
* Normalize a dependency value to its numeric parent task ID.
* - Numbers are returned as-is (if finite)
* - Numeric strings are parsed ("5" -> 5)
* - Dotted strings return the parent portion ("5.2" -> 5)
* - Empty/invalid values return null
* - null/undefined are preserved
* @param {number|string|null|undefined} dep
* @returns {number|null|undefined}
*/
function normalizeDependency(dep) {
if (dep === null || dep === undefined) return dep;
if (typeof dep === 'number') return Number.isFinite(dep) ? dep : null;
if (typeof dep === 'string') {
const trimmed = dep.trim();
if (trimmed === '') return null;
const parentPart = trimmed.includes('.') ? trimmed.split('.')[0] : trimmed;
const parsed = parseInt(parentPart, 10);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
/**
* Normalize an array of dependency values to numeric IDs.
* Preserves null/undefined input (returns as-is) and filters out invalid entries.
* @param {Array<any>|null|undefined} deps
* @returns {Array<number>|null|undefined}
*/
function normalizeDependencies(deps) {
if (deps === null || deps === undefined) return deps;
if (!Array.isArray(deps)) return deps;
return deps
.map((d) => normalizeDependency(d))
.filter((n) => Number.isFinite(n));
}
/** /**
* Move one or more tasks/subtasks to new positions * Move one or more tasks/subtasks to new positions
* @param {string} tasksPath - Path to tasks.json file * @param {string} tasksPath - Path to tasks.json file
@@ -711,34 +674,15 @@ async function resolveDependencies(
) { ) {
const { withDependencies = false, ignoreDependencies = false } = options; const { withDependencies = false, ignoreDependencies = false } = options;
// Scope allTasks to the source tag to avoid cross-tag contamination when
// computing dependency chains for --with-dependencies
const tasksInSourceTag = Array.isArray(allTasks)
? allTasks.filter((t) => t && t.tag === sourceTag)
: [];
// Handle --with-dependencies flag first (regardless of cross-tag dependencies) // Handle --with-dependencies flag first (regardless of cross-tag dependencies)
if (withDependencies) { if (withDependencies) {
// Move dependent tasks along with main tasks // Move dependent tasks along with main tasks
// Find ALL dependencies recursively, but only using tasks from the source tag // Find ALL dependencies recursively within the same tag
const allDependentTaskIdsRaw = findAllDependenciesRecursively( const allDependentTaskIds = findAllDependenciesRecursively(
sourceTasks, sourceTasks,
tasksInSourceTag, allTasks,
{ maxDepth: 100, includeSelf: false } { maxDepth: 100, includeSelf: false }
); );
// Filter dependent IDs to those that actually exist in the source tag
const sourceTagIds = new Set(
tasksInSourceTag.map((t) =>
typeof t.id === 'string' ? parseInt(t.id, 10) : t.id
)
);
const allDependentTaskIds = allDependentTaskIdsRaw.filter((depId) => {
// Only numeric task IDs are eligible to be moved (subtasks cannot be moved cross-tag)
const normalizedId = normalizeDependency(depId);
return Number.isFinite(normalizedId) && sourceTagIds.has(normalizedId);
});
const allTaskIdsToMove = [...new Set([...taskIds, ...allDependentTaskIds])]; const allTaskIdsToMove = [...new Set([...taskIds, ...allDependentTaskIds])];
log( log(
@@ -767,31 +711,22 @@ async function resolveDependencies(
if (ignoreDependencies) { if (ignoreDependencies) {
// Break cross-tag dependencies (edge case - shouldn't normally happen) // Break cross-tag dependencies (edge case - shouldn't normally happen)
sourceTasks.forEach((task) => { sourceTasks.forEach((task) => {
const sourceTagTasks = tasksInSourceTag;
const targetTagTasks = Array.isArray(allTasks)
? allTasks.filter((t) => t && t.tag === targetTag)
: [];
task.dependencies = task.dependencies.filter((depId) => { task.dependencies = task.dependencies.filter((depId) => {
const parentTaskId = normalizeDependency(depId); // Handle both task IDs and subtask IDs (e.g., "1.2")
let depTask = null;
// If dependency resolves to a task in the source tag, drop it (would be cross-tag after move) if (typeof depId === 'string' && depId.includes('.')) {
if ( // It's a subtask ID - extract parent task ID and find the parent task
Number.isFinite(parentTaskId) && const [parentId, subtaskId] = depId
sourceTagTasks.some((t) => t.id === parentTaskId) .split('.')
) { .map((id) => parseInt(id, 10));
return false; depTask = allTasks.find((t) => t.id === parentId);
} else {
// It's a regular task ID - normalize to number for comparison
const normalizedDepId =
typeof depId === 'string' ? parseInt(depId, 10) : depId;
depTask = allTasks.find((t) => t.id === normalizedDepId);
} }
return !depTask || depTask.tag === targetTag;
// If dependency resolves to a task in the target tag, keep it
if (
Number.isFinite(parentTaskId) &&
targetTagTasks.some((t) => t.id === parentTaskId)
) {
return true;
}
// Otherwise, keep as-is (unknown/unresolved dependency)
return true;
}); });
}); });
@@ -875,16 +810,7 @@ async function executeMoveOperation(
if (existingTaskIndex !== -1) { if (existingTaskIndex !== -1) {
throw new MoveTaskError( throw new MoveTaskError(
MOVE_ERROR_CODES.TASK_ALREADY_EXISTS, MOVE_ERROR_CODES.TASK_ALREADY_EXISTS,
`Task ${taskId} already exists in target tag "${targetTag}"`, `Task ${taskId} already exists in target tag "${targetTag}"`
{
conflictingId: normalizedTaskId,
targetTag,
suggestions: [
'Choose a different target tag without conflicting IDs',
'Move a different set of IDs (avoid existing ones)',
'If needed, move within-tag to a new ID first, then cross-tag move'
]
}
); );
} }
@@ -925,8 +851,7 @@ async function finalizeMove(
tasksPath, tasksPath,
context, context,
sourceTag, sourceTag,
targetTag, targetTag
dependencyResolution
) { ) {
const { projectRoot } = context; const { projectRoot } = context;
const { rawData, movedTasks } = moveResult; const { rawData, movedTasks } = moveResult;
@@ -934,23 +859,10 @@ async function finalizeMove(
// Write the updated data // Write the updated data
writeJSON(tasksPath, rawData, projectRoot, null); writeJSON(tasksPath, rawData, projectRoot, null);
const response = { return {
message: `Successfully moved ${movedTasks.length} tasks from "${sourceTag}" to "${targetTag}"`, message: `Successfully moved ${movedTasks.length} tasks from "${sourceTag}" to "${targetTag}"`,
movedTasks movedTasks
}; };
// If we intentionally broke cross-tag dependencies, provide tips to validate & fix
if (
dependencyResolution &&
dependencyResolution.type === 'ignored-dependencies'
) {
response.tips = [
'Run "task-master validate-dependencies" to check for dependency issues.',
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
];
}
return response;
} }
/** /**
@@ -986,7 +898,7 @@ async function moveTasksBetweenTags(
const { rawData, sourceTasks, allTasks } = await prepareTaskData(validation); const { rawData, sourceTasks, allTasks } = await prepareTaskData(validation);
// 3. Handle dependencies // 3. Handle dependencies
const { tasksToMove, dependencyResolution } = await resolveDependencies( const { tasksToMove } = await resolveDependencies(
sourceTasks, sourceTasks,
allTasks, allTasks,
options, options,
@@ -1011,8 +923,7 @@ async function moveTasksBetweenTags(
tasksPath, tasksPath,
context, context,
sourceTag, sourceTag,
targetTag, targetTag
dependencyResolution
); );
} }

View File

@@ -77,7 +77,7 @@ export class PrdParseConfig {
* Check if codebase analysis is available (Claude Code or Gemini CLI) * Check if codebase analysis is available (Claude Code or Gemini CLI)
*/ */
hasCodebaseAnalysis() { hasCodebaseAnalysis() {
return hasCodebaseAnalysis(this.research, this.projectRoot, this.session); return hasCodebaseAnalysis(this.research, this.projectRoot);
} }
} }

View File

@@ -232,11 +232,7 @@ async function updateSubtaskById(
updatePrompt: prompt, updatePrompt: prompt,
useResearch: useResearch, useResearch: useResearch,
gatheredContext: gatheredContext || '', gatheredContext: gatheredContext || '',
hasCodebaseAnalysis: hasCodebaseAnalysis( hasCodebaseAnalysis: hasCodebaseAnalysis(useResearch, projectRoot),
useResearch,
projectRoot,
session
),
projectRoot: projectRoot projectRoot: projectRoot
}; };

View File

@@ -458,11 +458,7 @@ async function updateTaskById(
useResearch: useResearch, useResearch: useResearch,
currentDetails: taskToUpdate.details || '(No existing details)', currentDetails: taskToUpdate.details || '(No existing details)',
gatheredContext: gatheredContext || '', gatheredContext: gatheredContext || '',
hasCodebaseAnalysis: hasCodebaseAnalysis( hasCodebaseAnalysis: hasCodebaseAnalysis(useResearch, projectRoot),
useResearch,
projectRoot,
session
),
projectRoot: projectRoot projectRoot: projectRoot
}; };

View File

@@ -436,11 +436,7 @@ async function updateTasks(
updatePrompt: prompt, updatePrompt: prompt,
useResearch, useResearch,
projectContext: gatheredContext, projectContext: gatheredContext,
hasCodebaseAnalysis: hasCodebaseAnalysis( hasCodebaseAnalysis: hasCodebaseAnalysis(useResearch, projectRoot),
useResearch,
projectRoot,
session
),
projectRoot: projectRoot projectRoot: projectRoot
} }
); );

View File

@@ -2876,6 +2876,9 @@ export function displayCrossTagDependencyError(
` 4. Move dependencies first: task-master move --from=${conflicts.map((c) => c.dependencyId).join(',')} --from-tag=${conflicts[0].dependencyTag} --to-tag=${targetTag}` ` 4. Move dependencies first: task-master move --from=${conflicts.map((c) => c.dependencyId).join(',')} --from-tag=${conflicts[0].dependencyTag} --to-tag=${targetTag}`
); );
} }
console.log(
` 5. Force move (may break dependencies): task-master move --from=${sourceIds} --from-tag=${sourceTag} --to-tag=${targetTag} --force`
);
} }
/** /**

View File

@@ -158,25 +158,20 @@ describe('Cross-Tag Move CLI Integration', () => {
); );
try { try {
const result = await moveTaskModule.moveTasksBetweenTags( await moveTaskModule.moveTasksBetweenTags(
tasksPath, tasksPath,
taskIds, taskIds,
sourceTag, sourceTag,
toTag, toTag,
{ {
withDependencies, withDependencies,
ignoreDependencies ignoreDependencies,
force
} }
); );
console.log(chalk.green('Successfully moved task(s) between tags')); console.log(chalk.green('Successfully moved task(s) between tags'));
// Print advisory tips when present
if (result && Array.isArray(result.tips) && result.tips.length > 0) {
console.log('Next Steps:');
result.tips.forEach((t) => console.log(`${t}`));
}
// Generate task files for both tags // Generate task files for both tags
await generateTaskFilesModule.default( await generateTaskFilesModule.default(
tasksPath, tasksPath,
@@ -190,21 +185,6 @@ describe('Cross-Tag Move CLI Integration', () => {
); );
} catch (error) { } catch (error) {
console.error(chalk.red(`Error: ${error.message}`)); console.error(chalk.red(`Error: ${error.message}`));
// Print ID collision guidance similar to CLI help block
if (
typeof error?.message === 'string' &&
error.message.includes('already exists in target tag')
) {
console.log('');
console.log('Conflict: ID already exists in target tag');
console.log(
' • Choose a different target tag without conflicting IDs'
);
console.log(' • Move a different set of IDs (avoid existing ones)');
console.log(
' • If needed, move within-tag to a new ID first, then cross-tag move'
);
}
throw error; throw error;
} }
} else { } else {
@@ -288,7 +268,8 @@ describe('Cross-Tag Move CLI Integration', () => {
'in-progress', 'in-progress',
{ {
withDependencies: undefined, withDependencies: undefined,
ignoreDependencies: undefined ignoreDependencies: undefined,
force: undefined
} }
); );
}); });
@@ -342,7 +323,8 @@ describe('Cross-Tag Move CLI Integration', () => {
'in-progress', 'in-progress',
{ {
withDependencies: true, withDependencies: true,
ignoreDependencies: undefined ignoreDependencies: undefined,
force: undefined
} }
); );
}); });
@@ -368,7 +350,8 @@ describe('Cross-Tag Move CLI Integration', () => {
'in-progress', 'in-progress',
{ {
withDependencies: undefined, withDependencies: undefined,
ignoreDependencies: true ignoreDependencies: true,
force: undefined
} }
); );
}); });
@@ -393,7 +376,8 @@ describe('Cross-Tag Move CLI Integration', () => {
'new-tag', 'new-tag',
{ {
withDependencies: undefined, withDependencies: undefined,
ignoreDependencies: undefined ignoreDependencies: undefined,
force: undefined
} }
); );
}); });
@@ -456,57 +440,6 @@ describe('Cross-Tag Move CLI Integration', () => {
restore(); restore();
}); });
it('should print advisory tips when result.tips are returned (ignore-dependencies)', async () => {
const { errorMessages, logMessages, restore } = captureConsoleAndExit();
try {
// Arrange: mock move to return tips
mockMoveTasksBetweenTags.mockResolvedValue({
message: 'ok',
tips: [
'Run "task-master validate-dependencies" to check for dependency issues.',
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
]
});
await moveAction({
from: '2',
fromTag: 'backlog',
toTag: 'in-progress',
ignoreDependencies: true
});
const joined = logMessages.join('\n');
expect(joined).toContain('Next Steps');
expect(joined).toContain('validate-dependencies');
expect(joined).toContain('fix-dependencies');
} finally {
restore();
}
});
it('should print ID collision suggestions when target already has the ID', async () => {
const { errorMessages, logMessages, restore } = captureConsoleAndExit();
try {
// Arrange: mock move to throw collision
const err = new Error(
'Task 1 already exists in target tag "in-progress"'
);
mockMoveTasksBetweenTags.mockRejectedValue(err);
await expect(
moveAction({ from: '1', fromTag: 'backlog', toTag: 'in-progress' })
).rejects.toThrow('already exists in target tag');
const joined = logMessages.join('\n');
expect(joined).toContain('Conflict: ID already exists in target tag');
expect(joined).toContain('different target tag');
expect(joined).toContain('different set of IDs');
expect(joined).toContain('within-tag');
} finally {
restore();
}
});
it('should handle same tag error correctly', async () => { it('should handle same tag error correctly', async () => {
const options = { const options = {
from: '1', from: '1',
@@ -552,7 +485,8 @@ describe('Cross-Tag Move CLI Integration', () => {
from: '1', from: '1',
toTag: 'in-progress', toTag: 'in-progress',
withDependencies: false, withDependencies: false,
ignoreDependencies: false ignoreDependencies: false,
force: false
// fromTag is intentionally not provided to test fallback // fromTag is intentionally not provided to test fallback
}); });
@@ -564,7 +498,8 @@ describe('Cross-Tag Move CLI Integration', () => {
'in-progress', 'in-progress',
{ {
withDependencies: false, withDependencies: false,
ignoreDependencies: false ignoreDependencies: false,
force: false
} }
); );
@@ -601,7 +536,8 @@ describe('Cross-Tag Move CLI Integration', () => {
'in-progress', 'in-progress',
{ {
withDependencies: undefined, withDependencies: undefined,
ignoreDependencies: undefined ignoreDependencies: undefined,
force: undefined
} }
); );
@@ -619,7 +555,32 @@ describe('Cross-Tag Move CLI Integration', () => {
); );
}); });
// Note: --force flag is no longer supported for cross-tag moves it('should handle --force flag correctly', async () => {
// Mock successful cross-tag move with force flag
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '1',
fromTag: 'backlog',
toTag: 'in-progress',
force: true
};
await moveAction(options);
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
expect.stringContaining('tasks.json'),
[1],
'backlog',
'in-progress',
{
withDependencies: undefined,
ignoreDependencies: undefined,
force: true // Force flag should be passed through
}
);
});
it('should fail when invalid task ID is provided', async () => { it('should fail when invalid task ID is provided', async () => {
const options = { const options = {
@@ -701,11 +662,90 @@ describe('Cross-Tag Move CLI Integration', () => {
restore(); restore();
}); });
// Note: --force combinations removed it('should combine --with-dependencies and --force flags correctly', async () => {
// Mock successful cross-tag move with both flags
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
// Note: --force combinations removed const options = {
from: '1,2',
fromTag: 'backlog',
toTag: 'in-progress',
withDependencies: true,
force: true
};
// Note: --force combinations removed await moveAction(options);
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
expect.stringContaining('tasks.json'),
[1, 2],
'backlog',
'in-progress',
{
withDependencies: true,
ignoreDependencies: undefined,
force: true // Both flags should be passed
}
);
});
it('should combine --ignore-dependencies and --force flags correctly', async () => {
// Mock successful cross-tag move with both flags
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '1',
fromTag: 'backlog',
toTag: 'in-progress',
ignoreDependencies: true,
force: true
};
await moveAction(options);
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
expect.stringContaining('tasks.json'),
[1],
'backlog',
'in-progress',
{
withDependencies: undefined,
ignoreDependencies: true,
force: true // Both flags should be passed
}
);
});
it('should handle all three flags combined correctly', async () => {
// Mock successful cross-tag move with all flags
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
mockGenerateTaskFiles.mockResolvedValue(undefined);
const options = {
from: '1,2,3',
fromTag: 'backlog',
toTag: 'in-progress',
withDependencies: true,
ignoreDependencies: true,
force: true
};
await moveAction(options);
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
expect.stringContaining('tasks.json'),
[1, 2, 3],
'backlog',
'in-progress',
{
withDependencies: true,
ignoreDependencies: true,
force: true // All three flags should be passed
}
);
});
it('should handle whitespace in comma-separated task IDs', async () => { it('should handle whitespace in comma-separated task IDs', async () => {
// Mock successful cross-tag move with whitespace // Mock successful cross-tag move with whitespace

View File

@@ -426,38 +426,6 @@ describe('Cross-Tag Task Movement Integration Tests', () => {
); );
}); });
it('should provide advisory tips when ignoreDependencies breaks deps', async () => {
// Move a task that has dependencies so cross-tag conflicts would be broken
const taskIds = [2]; // backlog:2 depends on 1
const sourceTag = 'backlog';
const targetTag = 'in-progress';
// Override cross-tag detection to simulate conflicts for this case
const depManager = await import(
'../../scripts/modules/dependency-manager.js'
);
depManager.findCrossTagDependencies.mockReturnValueOnce([
{ taskId: 2, dependencyId: 1, dependencyTag: sourceTag }
]);
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{ ignoreDependencies: true },
{ projectRoot: '/test/project' }
);
expect(Array.isArray(result.tips)).toBe(true);
const expectedTips = [
'Run "task-master validate-dependencies" to check for dependency issues.',
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
];
expect(result.tips).toHaveLength(expectedTips.length);
expect(result.tips).toEqual(expect.arrayContaining(expectedTips));
});
it('should move task without cross-tag dependency conflicts (since dependencies only exist within tags)', async () => { it('should move task without cross-tag dependency conflicts (since dependencies only exist within tags)', async () => {
const taskIds = [2]; // Task 2 depends on Task 1 (both in same tag) const taskIds = [2]; // Task 2 depends on Task 1 (both in same tag)
const sourceTag = 'backlog'; const sourceTag = 'backlog';
@@ -596,25 +564,6 @@ describe('Cross-Tag Task Movement Integration Tests', () => {
{ projectRoot: '/test/project' } { projectRoot: '/test/project' }
) )
).rejects.toThrow('Task 1 already exists in target tag "in-progress"'); ).rejects.toThrow('Task 1 already exists in target tag "in-progress"');
// Validate suggestions on the error payload
try {
await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
} catch (err) {
expect(err.code).toBe('TASK_ALREADY_EXISTS');
expect(Array.isArray(err.data?.suggestions)).toBe(true);
const s = (err.data?.suggestions || []).join(' ');
expect(s).toContain('different target tag');
expect(s).toContain('different set of IDs');
expect(s).toContain('within-tag');
}
}); });
}); });
@@ -688,7 +637,37 @@ describe('Cross-Tag Task Movement Integration Tests', () => {
); );
}); });
// Note: force flag deprecated for cross-tag moves; covered by with/ignore dependencies tests it('should handle force flag for dependency conflicts', async () => {
const taskIds = [2]; // Task 2 depends on Task 1
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{ force: true },
{ projectRoot: '/test/project' }
);
// Verify task was moved despite dependency conflicts
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 2,
tag: 'in-progress'
})
])
})
}),
'/test/project',
null
);
});
}); });
describe('Complex Scenarios', () => { describe('Complex Scenarios', () => {

View File

@@ -145,7 +145,6 @@ const DEFAULT_CONFIG = {
projectName: 'Task Master', projectName: 'Task Master',
ollamaBaseURL: 'http://localhost:11434/api', ollamaBaseURL: 'http://localhost:11434/api',
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com', bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
enableCodebaseAnalysis: true,
responseLanguage: 'English' responseLanguage: 'English'
}, },
claudeCode: {} claudeCode: {}

View File

@@ -1,135 +0,0 @@
import { jest } from '@jest/globals';
// Mocks
const mockFindTasksPath = jest
.fn()
.mockReturnValue('/test/path/.taskmaster/tasks/tasks.json');
jest.unstable_mockModule(
'../../../../mcp-server/src/core/utils/path-utils.js',
() => ({
findTasksPath: mockFindTasksPath
})
);
const mockEnableSilentMode = jest.fn();
const mockDisableSilentMode = jest.fn();
jest.unstable_mockModule('../../../../scripts/modules/utils.js', () => ({
enableSilentMode: mockEnableSilentMode,
disableSilentMode: mockDisableSilentMode
}));
// Spyable mock for moveTasksBetweenTags
const mockMoveTasksBetweenTags = jest.fn();
jest.unstable_mockModule(
'../../../../scripts/modules/task-manager/move-task.js',
() => ({
moveTasksBetweenTags: mockMoveTasksBetweenTags
})
);
// Import after mocks
const { moveTaskCrossTagDirect } = await import(
'../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js'
);
describe('MCP Cross-Tag Move Direct Function - options & suggestions', () => {
const mockLog = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
beforeEach(() => {
jest.clearAllMocks();
});
it('passes only withDependencies/ignoreDependencies (no force) to core', async () => {
// Arrange: make core throw tag validation after call to capture params
mockMoveTasksBetweenTags.mockImplementation(() => {
const err = new Error('Source tag "invalid" not found or invalid');
err.code = 'INVALID_SOURCE_TAG';
throw err;
});
// Act
await moveTaskCrossTagDirect(
{
sourceIds: '1,2',
sourceTag: 'backlog',
targetTag: 'in-progress',
withDependencies: true,
projectRoot: '/test'
},
mockLog
);
// Assert options argument (5th param)
expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
const args = mockMoveTasksBetweenTags.mock.calls[0];
const moveOptions = args[4];
expect(moveOptions).toEqual({
withDependencies: true,
ignoreDependencies: false
});
expect('force' in moveOptions).toBe(false);
});
it('returns conflict suggestions on cross-tag dependency conflicts', async () => {
// Arrange: core throws cross-tag dependency conflicts
mockMoveTasksBetweenTags.mockImplementation(() => {
const err = new Error(
'Cannot move tasks: 2 cross-tag dependency conflicts found'
);
err.code = 'CROSS_TAG_DEPENDENCY_CONFLICTS';
throw err;
});
// Act
const result = await moveTaskCrossTagDirect(
{
sourceIds: '1',
sourceTag: 'backlog',
targetTag: 'in-progress',
projectRoot: '/test'
},
mockLog
);
// Assert
expect(result.success).toBe(false);
expect(result.error.code).toBe('CROSS_TAG_DEPENDENCY_CONFLICT');
expect(Array.isArray(result.error.suggestions)).toBe(true);
// Key suggestions
const s = result.error.suggestions.join(' ');
expect(s).toContain('--with-dependencies');
expect(s).toContain('--ignore-dependencies');
expect(s).toContain('validate-dependencies');
expect(s).toContain('Move dependencies first');
});
it('returns ID collision suggestions when target tag already has the ID', async () => {
// Arrange: core throws TASK_ALREADY_EXISTS structured error
mockMoveTasksBetweenTags.mockImplementation(() => {
const err = new Error(
'Task 1 already exists in target tag "in-progress"'
);
err.code = 'TASK_ALREADY_EXISTS';
throw err;
});
// Act
const result = await moveTaskCrossTagDirect(
{
sourceIds: '1',
sourceTag: 'backlog',
targetTag: 'in-progress',
projectRoot: '/test'
},
mockLog
);
// Assert
expect(result.success).toBe(false);
expect(result.error.code).toBe('TASK_ALREADY_EXISTS');
const joined = (result.error.suggestions || []).join(' ');
expect(joined).toContain('different target tag');
expect(joined).toContain('different set of IDs');
expect(joined).toContain('within-tag');
});
});

View File

@@ -6,6 +6,7 @@ import {
createGetTagAwareFilePathMock, createGetTagAwareFilePathMock,
createSlugifyTagForFilePathMock createSlugifyTagForFilePathMock
} from './setup.js'; } from './setup.js';
import { hasCodebaseAnalysis } from '../../../../../scripts/modules/config-manager.js';
// Mock the dependencies before importing the module under test // Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({

View File

@@ -6,6 +6,7 @@
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { hasCodebaseAnalysis } from '../../../../../scripts/modules/config-manager.js';
// Mock the dependencies // Mock the dependencies
jest.unstable_mockModule('../../../../../src/utils/path-utils.js', () => ({ jest.unstable_mockModule('../../../../../src/utils/path-utils.js', () => ({

View File

@@ -219,37 +219,6 @@ describe('Cross-Tag Task Movement', () => {
}); });
}); });
// New test: ensure with-dependencies only traverses tasks from the source tag
it('should scope dependency traversal to source tag when using --with-dependencies', async () => {
findCrossTagDependencies.mockReturnValue([]);
validateSubtaskMove.mockImplementation(() => {});
const result = await moveTasksBetweenTags(
mockTasksPath,
[1], // backlog:1 depends on backlog:2
'backlog',
'in-progress',
{ withDependencies: true },
mockContext
);
// Write should include backlog:2 moved, and must NOT traverse or fetch dependencies from the target tag
expect(writeJSON).toHaveBeenCalledWith(
mockTasksPath,
expect.objectContaining({
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1 }),
expect.objectContaining({ id: 2 }) // the backlog:2 now moved
// ensure existing in-progress:2 remains (by id) but we don't double-add or fetch deps from it
])
})
}),
mockContext.projectRoot,
null
);
});
describe('moveTasksBetweenTags', () => { describe('moveTasksBetweenTags', () => {
it('should move tasks without dependencies successfully', async () => { it('should move tasks without dependencies successfully', async () => {
// Mock the dependency functions to return no conflicts // Mock the dependency functions to return no conflicts