Compare commits
16 Commits
feat-gener
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb84f9660 | ||
|
|
7b5a7c4495 | ||
|
|
caee040907 | ||
|
|
4b5473860b | ||
|
|
b43b7ce201 | ||
|
|
86027f1ee4 | ||
|
|
4f984f8a69 | ||
|
|
f7646f41b5 | ||
|
|
20004a39ea | ||
|
|
f1393f47b1 | ||
|
|
738ec51c04 | ||
|
|
c7418c4594 | ||
|
|
0747f1c772 | ||
|
|
ffe24a2e35 | ||
|
|
3b3dbabed1 | ||
|
|
af53525cbc |
11
.changeset/brave-lions-sing.md
Normal file
11
.changeset/brave-lions-sing.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add Codex CLI provider with OAuth authentication
|
||||
|
||||
- Added codex-cli provider for GPT-5 and GPT-5-Codex models (272K input / 128K output)
|
||||
- OAuth-first authentication via `codex login` - no API key required
|
||||
- Optional OPENAI_CODEX_API_KEY support
|
||||
- Codebase analysis capabilities automatically enabled
|
||||
- Command-specific settings and approval/sandbox modes
|
||||
5
.changeset/easy-spiders-wave.md
Normal file
5
.changeset/easy-spiders-wave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Do a quick fix on build
|
||||
5
.changeset/fix-mcp-connection-errors.md
Normal file
5
.changeset/fix-mcp-connection-errors.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.
|
||||
5
.changeset/fix-mcp-default-tasks-path.md
Normal file
5
.changeset/fix-mcp-default-tasks-path.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
|
||||
30
.changeset/gentle-cats-dance.md
Normal file
30
.changeset/gentle-cats-dance.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Migrate AI services to use generateObject for structured data generation
|
||||
|
||||
This update migrates all AI service calls from generateText to generateObject, ensuring more reliable and structured responses across all commands.
|
||||
|
||||
### Key Changes:
|
||||
|
||||
- **Unified AI Service**: Replaced separate generateText implementations with a single generateObjectService that handles structured data generation
|
||||
- **JSON Mode Support**: Added proper JSON mode configuration for providers that support it (OpenAI, Anthropic, Google, Groq)
|
||||
- **Schema Validation**: Integrated Zod schemas for all AI-generated content with automatic validation
|
||||
- **Provider Compatibility**: Maintained compatibility with all existing providers while leveraging their native structured output capabilities
|
||||
- **Improved Reliability**: Structured output generation reduces parsing errors and ensures consistent data formats
|
||||
|
||||
### Technical Improvements:
|
||||
|
||||
- Centralized provider configuration in `ai-providers-unified.js`
|
||||
- Added `generateObject` support detection for each provider
|
||||
- Implemented proper error handling for schema validation failures
|
||||
- Maintained backward compatibility with existing prompt structures
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
- Fixed subtask ID numbering issue where AI was generating inconsistent IDs (101-105, 601-603) instead of sequential numbering (1, 2, 3...)
|
||||
- Enhanced prompt instructions to enforce proper ID generation patterns
|
||||
- Ensured subtasks display correctly as X.1, X.2, X.3 format
|
||||
|
||||
This migration improves the reliability and consistency of AI-generated content throughout the Task Master application.
|
||||
23
.changeset/pre.json
Normal file
23
.changeset/pre.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "rc",
|
||||
"initialVersions": {
|
||||
"task-master-ai": "0.27.3",
|
||||
"docs": "0.0.4",
|
||||
"extension": "0.25.4"
|
||||
},
|
||||
"changesets": [
|
||||
"chore-fix-docs",
|
||||
"cursor-slash-commands",
|
||||
"curvy-weeks-flow",
|
||||
"easy-spiders-wave",
|
||||
"flat-cities-say",
|
||||
"forty-tables-invite",
|
||||
"gentle-cats-dance",
|
||||
"mcp-timeout-configuration",
|
||||
"petite-ideas-grab",
|
||||
"silly-pandas-find",
|
||||
"sweet-maps-rule",
|
||||
"whole-pigs-say"
|
||||
]
|
||||
}
|
||||
8
.changeset/whole-pigs-say.md
Normal file
8
.changeset/whole-pigs-say.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix complexity score not showing for `task-master show` and `task-master list`
|
||||
|
||||
- Added complexity score on "next task" when running `task-master list`
|
||||
- Added colors to complexity to reflect complexity (easy, medium, hard)
|
||||
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,5 +1,84 @@
|
||||
# task-master-ai
|
||||
|
||||
## 0.28.0-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1274](https://github.com/eyaltoledano/claude-task-master/pull/1274) [`4f984f8`](https://github.com/eyaltoledano/claude-task-master/commit/4f984f8a6965da9f9c7edd60ddfd6560ac022917) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Do a quick fix on build
|
||||
|
||||
## 0.28.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1215](https://github.com/eyaltoledano/claude-task-master/pull/1215) [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d) Thanks [@joedanz](https://github.com/joedanz)! - Add Cursor IDE custom slash command support
|
||||
|
||||
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.
|
||||
|
||||
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Added api keys page on docs website: docs.task-master.dev/getting-started/api-keys
|
||||
|
||||
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Move to AI SDK v5:
|
||||
- Works better with claude-code and gemini-cli as ai providers
|
||||
- Improved openai model family compatibility
|
||||
- Migrate ollama provider to v2
|
||||
- Closes #1223, #1013, #1161, #1174
|
||||
|
||||
- [#1262](https://github.com/eyaltoledano/claude-task-master/pull/1262) [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Migrate AI services to use generateObject for structured data generation
|
||||
|
||||
This update migrates all AI service calls from generateText to generateObject, ensuring more reliable and structured responses across all commands.
|
||||
|
||||
### Key Changes:
|
||||
- **Unified AI Service**: Replaced separate generateText implementations with a single generateObjectService that handles structured data generation
|
||||
- **JSON Mode Support**: Added proper JSON mode configuration for providers that support it (OpenAI, Anthropic, Google, Groq)
|
||||
- **Schema Validation**: Integrated Zod schemas for all AI-generated content with automatic validation
|
||||
- **Provider Compatibility**: Maintained compatibility with all existing providers while leveraging their native structured output capabilities
|
||||
- **Improved Reliability**: Structured output generation reduces parsing errors and ensures consistent data formats
|
||||
|
||||
### Technical Improvements:
|
||||
- Centralized provider configuration in `ai-providers-unified.js`
|
||||
- Added `generateObject` support detection for each provider
|
||||
- Implemented proper error handling for schema validation failures
|
||||
- Maintained backward compatibility with existing prompt structures
|
||||
|
||||
### Bug Fixes:
|
||||
- Fixed subtask ID numbering issue where AI was generating inconsistent IDs (101-105, 601-603) instead of sequential numbering (1, 2, 3...)
|
||||
- Enhanced prompt instructions to enforce proper ID generation patterns
|
||||
- Ensured subtasks display correctly as X.1, X.2, X.3 format
|
||||
|
||||
This migration improves the reliability and consistency of AI-generated content throughout the Task Master application.
|
||||
|
||||
- [#1112](https://github.com/eyaltoledano/claude-task-master/pull/1112) [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541) Thanks [@olssonsten](https://github.com/olssonsten)! - Enhanced Roo Code profile with MCP timeout configuration for improved reliability during long-running AI operations. The Roo profile now automatically configures a 300-second timeout for MCP server operations, preventing timeouts during complex tasks like `parse-prd`, `expand-all`, `analyze-complexity`, and `research` operations. This change also replaces static MCP configuration files with programmatic generation for better maintainability.
|
||||
|
||||
**What's New:**
|
||||
- 300-second timeout for MCP operations (up from default 60 seconds)
|
||||
- Programmatic MCP configuration generation (replaces static asset files)
|
||||
- Enhanced reliability for AI-powered operations
|
||||
- Consistent with other AI coding assistant profiles
|
||||
|
||||
**Migration:** No user action required - existing Roo Code installations will automatically receive the enhanced MCP configuration on next initialization.
|
||||
|
||||
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Upgrade grok-cli ai provider to ai sdk v5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1235](https://github.com/eyaltoledano/claude-task-master/pull/1235) [`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve `analyze-complexity` cli docs and `--research` flag documentation
|
||||
|
||||
- [#1251](https://github.com/eyaltoledano/claude-task-master/pull/1251) [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Change parent task back to "pending" when all subtasks are in "pending" state
|
||||
|
||||
- [#1172](https://github.com/eyaltoledano/claude-task-master/pull/1172) [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d) Thanks [@jujax](https://github.com/jujax)! - Fix Claude Code settings validation for pathToClaudeCodeExecutable
|
||||
|
||||
- [#1192](https://github.com/eyaltoledano/claude-task-master/pull/1192) [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a) Thanks [@nukunga](https://github.com/nukunga)! - Fix sonar deep research model failing, should be called `sonar-deep-research`
|
||||
|
||||
- [#1270](https://github.com/eyaltoledano/claude-task-master/pull/1270) [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix complexity score not showing for `task-master show` and `task-master list`
|
||||
- Added complexity score on "next task" when running `task-master list`
|
||||
- Added colors to complexity to reflect complexity (easy, medium, hard)
|
||||
|
||||
## 0.27.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1254](https://github.com/eyaltoledano/claude-task-master/pull/1254) [`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed issue where `tm show` command could not find subtasks using dotted notation IDs (e.g., '8.1').
|
||||
- The command now properly searches within parent task subtasks and returns the correct subtask information.
|
||||
|
||||
## 0.27.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
22
CLAUDE.md
22
CLAUDE.md
@@ -4,6 +4,28 @@
|
||||
**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**
|
||||
@./.taskmaster/CLAUDE.md
|
||||
|
||||
## Test Guidelines
|
||||
|
||||
### Synchronous Tests
|
||||
- **NEVER use async/await in test functions** unless testing actual asynchronous operations
|
||||
- Use synchronous top-level imports instead of dynamic `await import()`
|
||||
- Test bodies should be synchronous whenever possible
|
||||
- Example:
|
||||
```javascript
|
||||
// ✅ CORRECT - Synchronous imports
|
||||
import { MyClass } from '../src/my-class.js';
|
||||
|
||||
it('should verify behavior', () => {
|
||||
expect(new MyClass().property).toBe(value);
|
||||
});
|
||||
|
||||
// ❌ INCORRECT - Async imports
|
||||
it('should verify behavior', async () => {
|
||||
const { MyClass } = await import('../src/my-class.js');
|
||||
expect(new MyClass().property).toBe(value);
|
||||
});
|
||||
```
|
||||
|
||||
## Changeset Guidelines
|
||||
|
||||
- When creating changesets, remember that it's user-facing, meaning we don't have to get into the specifics of the code, but rather mention what the end-user is getting or fixing from this changeset.
|
||||
@@ -88,8 +88,9 @@ At least one (1) of the following is required:
|
||||
- xAI API Key (for research or main model)
|
||||
- OpenRouter API Key (for research or main model)
|
||||
- Claude Code (no API key required - requires Claude Code CLI)
|
||||
- Codex CLI (OAuth via ChatGPT subscription - requires Codex CLI)
|
||||
|
||||
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code). Adding all API keys enables you to seamlessly switch between model providers at will.
|
||||
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code or Codex CLI with OAuth). Adding all API keys enables you to seamlessly switch between model providers at will.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -281,9 +281,14 @@ export class ListTasksCommand extends Command {
|
||||
const priorityBreakdown = getPriorityBreakdown(tasks);
|
||||
|
||||
// Find next task following the same logic as findNextTask
|
||||
const nextTask = this.findNextTask(tasks);
|
||||
const nextTaskInfo = this.findNextTask(tasks);
|
||||
|
||||
// Display dashboard boxes
|
||||
// Get the full task object with complexity data already included
|
||||
const nextTask = nextTaskInfo
|
||||
? tasks.find((t) => String(t.id) === String(nextTaskInfo.id))
|
||||
: undefined;
|
||||
|
||||
// Display dashboard boxes (nextTask already has complexity from storage enrichment)
|
||||
displayDashboards(
|
||||
taskStats,
|
||||
subtaskStats,
|
||||
@@ -303,14 +308,16 @@ export class ListTasksCommand extends Command {
|
||||
|
||||
// Display recommended next task section immediately after table
|
||||
if (nextTask) {
|
||||
// Find the full task object to get description
|
||||
const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id));
|
||||
const description = fullTask ? getTaskDescription(fullTask) : undefined;
|
||||
const description = getTaskDescription(nextTask);
|
||||
|
||||
displayRecommendedNextTask({
|
||||
...nextTask,
|
||||
status: 'pending', // Next task is typically pending
|
||||
description
|
||||
id: nextTask.id,
|
||||
title: nextTask.title,
|
||||
priority: nextTask.priority,
|
||||
status: nextTask.status,
|
||||
dependencies: nextTask.dependencies,
|
||||
description,
|
||||
complexity: nextTask.complexity as number | undefined
|
||||
});
|
||||
} else {
|
||||
displayRecommendedNextTask(undefined);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import type { Task, TaskPriority } from '@tm/core/types';
|
||||
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
* Statistics for task collection
|
||||
@@ -479,7 +480,7 @@ export function displayDependencyDashboard(
|
||||
? chalk.cyan(nextTask.dependencies.join(', '))
|
||||
: chalk.gray('None')
|
||||
}\n` +
|
||||
`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
|
||||
`Complexity: ${nextTask?.complexity !== undefined ? getComplexityWithColor(nextTask.complexity) : chalk.gray('N/A')}`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import type { Task } from '@tm/core/types';
|
||||
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
* Next task display options
|
||||
@@ -17,6 +18,7 @@ export interface NextTaskDisplayOptions {
|
||||
status?: string;
|
||||
dependencies?: (string | number)[];
|
||||
description?: string;
|
||||
complexity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +84,11 @@ export function displayRecommendedNextTask(
|
||||
: chalk.cyan(task.dependencies.join(', '));
|
||||
content.push(`Dependencies: ${depsDisplay}`);
|
||||
|
||||
// Complexity with color and label
|
||||
if (typeof task.complexity === 'number') {
|
||||
content.push(`Complexity: ${getComplexityWithColor(task.complexity)}`);
|
||||
}
|
||||
|
||||
// Description if available
|
||||
if (task.description) {
|
||||
content.push('');
|
||||
|
||||
@@ -9,7 +9,11 @@ import Table from 'cli-table3';
|
||||
import { marked, MarkedExtension } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
import type { Task } from '@tm/core/types';
|
||||
import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js';
|
||||
import {
|
||||
getStatusWithColor,
|
||||
getPriorityWithColor,
|
||||
getComplexityWithColor
|
||||
} from '../../utils/ui.js';
|
||||
|
||||
// Configure marked to use terminal renderer with subtle colors
|
||||
marked.use(
|
||||
@@ -108,7 +112,9 @@ export function displayTaskProperties(task: Task): void {
|
||||
getStatusWithColor(task.status),
|
||||
getPriorityWithColor(task.priority),
|
||||
deps,
|
||||
'N/A',
|
||||
typeof task.complexity === 'number'
|
||||
? getComplexityWithColor(task.complexity)
|
||||
: chalk.gray('N/A'),
|
||||
task.description || ''
|
||||
].join('\n');
|
||||
|
||||
|
||||
@@ -158,10 +158,18 @@ export function displayUpgradeNotification(
|
||||
export async function performAutoUpdate(
|
||||
latestVersion: string
|
||||
): Promise<boolean> {
|
||||
if (process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' || process.env.CI) {
|
||||
console.log(
|
||||
chalk.dim('Skipping auto-update (TASKMASTER_SKIP_AUTO_UPDATE/CI).')
|
||||
);
|
||||
if (
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' ||
|
||||
process.env.CI ||
|
||||
process.env.NODE_ENV === 'test'
|
||||
) {
|
||||
const reason =
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1'
|
||||
? 'TASKMASTER_SKIP_AUTO_UPDATE=1'
|
||||
: process.env.CI
|
||||
? 'CI environment'
|
||||
: 'NODE_ENV=test';
|
||||
console.log(chalk.dim(`Skipping auto-update (${reason})`));
|
||||
return false;
|
||||
}
|
||||
const spinner = ora({
|
||||
|
||||
@@ -84,7 +84,23 @@ export function getPriorityWithColor(priority: TaskPriority): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colored complexity display
|
||||
* Get complexity color and label based on score thresholds
|
||||
*/
|
||||
function getComplexityLevel(score: number): {
|
||||
color: (text: string) => string;
|
||||
label: string;
|
||||
} {
|
||||
if (score >= 7) {
|
||||
return { color: chalk.hex('#CC0000'), label: 'High' };
|
||||
} else if (score >= 4) {
|
||||
return { color: chalk.hex('#FF8800'), label: 'Medium' };
|
||||
} else {
|
||||
return { color: chalk.green, label: 'Low' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colored complexity display with dot indicator (simple format)
|
||||
*/
|
||||
export function getComplexityWithColor(complexity: number | string): string {
|
||||
const score =
|
||||
@@ -94,13 +110,20 @@ export function getComplexityWithColor(complexity: number | string): string {
|
||||
return chalk.gray('N/A');
|
||||
}
|
||||
|
||||
if (score >= 8) {
|
||||
return chalk.red.bold(`${score} (High)`);
|
||||
} else if (score >= 5) {
|
||||
return chalk.yellow(`${score} (Medium)`);
|
||||
} else {
|
||||
return chalk.green(`${score} (Low)`);
|
||||
const { color } = getComplexityLevel(score);
|
||||
return color(`● ${score}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colored complexity display with /10 format (for dashboards)
|
||||
*/
|
||||
export function getComplexityWithScore(complexity: number | undefined): string {
|
||||
if (typeof complexity !== 'number') {
|
||||
return chalk.gray('N/A');
|
||||
}
|
||||
|
||||
const { color, label } = getComplexityLevel(complexity);
|
||||
return color(`${complexity}/10 (${label})`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,8 +346,12 @@ export function createTaskTable(
|
||||
}
|
||||
|
||||
if (showComplexity) {
|
||||
// Show N/A if no complexity score
|
||||
row.push(chalk.gray('N/A'));
|
||||
// Show complexity score from report if available
|
||||
if (typeof task.complexity === 'number') {
|
||||
row.push(getComplexityWithColor(task.complexity));
|
||||
} else {
|
||||
row.push(chalk.gray('N/A'));
|
||||
}
|
||||
}
|
||||
|
||||
table.push(row);
|
||||
|
||||
@@ -85,7 +85,7 @@ The CLI is organized into a series of commands, each with its own set of options
|
||||
### 4. Project and Configuration
|
||||
|
||||
- **`init`**: Initializes a new project.
|
||||
- **`generate`**: Generates individual task files.
|
||||
- **`generate`**: Generates individual task files from tasks.json. Run this manually after task operations to create readable text files for each task.
|
||||
- **`migrate`**: Migrates a project to the new directory structure.
|
||||
- **`research`**: Performs AI-powered research.
|
||||
- `--query <query>`: The research query.
|
||||
@@ -123,7 +123,7 @@ The core functionalities can be categorized as follows:
|
||||
|
||||
### 1. Task and Subtask Management
|
||||
|
||||
These functions are the bread and butter of the application, allowing for the creation, modification, and deletion of tasks and subtasks.
|
||||
These functions are the bread and butter of the application, allowing for the creation, modification, and deletion of tasks and subtasks. Note: As of v0.27.3, these operations no longer automatically generate individual task files - use the `generate` command manually when needed.
|
||||
|
||||
- **`addTask(prompt, dependencies, priority)`**: Creates a new task using an AI-powered prompt to generate the title, description, details, and test strategy. It can also be used to create a task manually by providing the task data directly.
|
||||
- **`addSubtask(parentId, existingTaskId, newSubtaskData)`**: Adds a subtask to a parent task. It can either convert an existing task into a subtask or create a new subtask from scratch.
|
||||
@@ -167,7 +167,7 @@ These functions are crucial for managing the relationships between tasks.
|
||||
|
||||
These functions are for managing the project and its configuration.
|
||||
|
||||
- **`generateTaskFiles()`**: Generates individual task files from `tasks.json`.
|
||||
- **`generateTaskFiles()`**: Generates individual task files from `tasks.json`. This is now a manual operation - task management operations no longer automatically generate these files.
|
||||
- **`migrateProject()`**: Migrates the project to the new `.taskmaster` directory structure.
|
||||
- **`performResearch(query, options)`**: Performs AI-powered research with project context.
|
||||
|
||||
@@ -225,7 +225,7 @@ The MCP tools can be categorized in the same way as the core functionalities:
|
||||
### 5. Project and Configuration
|
||||
|
||||
- **`initialize_project`**: Initializes a new project.
|
||||
- **`generate`**: Generates individual task files.
|
||||
- **`generate`**: Generates individual task files from tasks.json. Run this manually when you want to create readable text files for each task.
|
||||
- **`models`**: Manages AI model configurations.
|
||||
- **`research`**: Performs AI-powered research.
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Change Log
|
||||
|
||||
## 0.25.5-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3), [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d), [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede), [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541), [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d), [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a), [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca), [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055)]:
|
||||
- task-master-ai@0.28.0-rc.0
|
||||
|
||||
## 0.25.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]:
|
||||
- task-master-ai@0.27.3
|
||||
|
||||
## 0.25.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.25.3",
|
||||
"version": "0.25.5-rc.0",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
|
||||
@@ -383,6 +383,12 @@ task-master models --set-main=my-local-llama --ollama
|
||||
# Set a custom OpenRouter model for the research role
|
||||
task-master models --set-research=google/gemini-pro --openrouter
|
||||
|
||||
# Set Codex CLI model for the main role (uses ChatGPT subscription via OAuth)
|
||||
task-master models --set-main=gpt-5-codex --codex-cli
|
||||
|
||||
# Set Codex CLI model for the fallback role
|
||||
task-master models --set-fallback=gpt-5 --codex-cli
|
||||
|
||||
# Run interactive setup to configure models, including custom ones
|
||||
task-master models --setup
|
||||
```
|
||||
|
||||
@@ -429,3 +429,153 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
||||
- Verify the deployment name matches your configuration exactly (case-sensitive)
|
||||
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
|
||||
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
|
||||
|
||||
### Codex CLI Provider
|
||||
|
||||
The Codex CLI provider integrates Task Master with OpenAI's Codex CLI, allowing you to use ChatGPT subscription models via OAuth authentication.
|
||||
|
||||
1. **Prerequisites**:
|
||||
- Node.js >= 18
|
||||
- Codex CLI >= 0.42.0 (>= 0.44.0 recommended)
|
||||
- ChatGPT subscription: Plus, Pro, Business, Edu, or Enterprise (for OAuth access to GPT-5 models)
|
||||
|
||||
2. **Installation**:
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
```
|
||||
|
||||
3. **Authentication** (OAuth - Primary Method):
|
||||
```bash
|
||||
codex login
|
||||
```
|
||||
This will open a browser window for OAuth authentication with your ChatGPT account. Once authenticated, Task Master will automatically use these credentials.
|
||||
|
||||
4. **Optional API Key Method**:
|
||||
While OAuth is the primary and recommended authentication method, you can optionally set an OpenAI API key:
|
||||
```bash
|
||||
# In .env file
|
||||
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||
```
|
||||
**Note**: The API key will only be injected if explicitly provided. OAuth is always preferred.
|
||||
|
||||
5. **Configuration**:
|
||||
```json
|
||||
// In .taskmaster/config.json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5-codex",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
"codexCli": {
|
||||
"allowNpx": true,
|
||||
"skipGitRepoCheck": true,
|
||||
"approvalMode": "on-failure",
|
||||
"sandboxMode": "workspace-write"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Available Models**:
|
||||
- `gpt-5` - Latest GPT-5 model (272K max input, 128K max output)
|
||||
- `gpt-5-codex` - GPT-5 optimized for agentic software engineering (272K max input, 128K max output)
|
||||
|
||||
7. **Codex CLI Settings (`codexCli` section)**:
|
||||
|
||||
The `codexCli` section in your configuration file supports the following options:
|
||||
|
||||
- **`allowNpx`** (boolean, default: `false`): Allow fallback to `npx @openai/codex` if CLI not found on PATH
|
||||
- **`skipGitRepoCheck`** (boolean, default: `false`): Skip git repository safety check (recommended for CI/non-repo usage)
|
||||
- **`approvalMode`** (string): Control command execution approval
|
||||
- `"untrusted"`: Require approval for all commands
|
||||
- `"on-failure"`: Only require approval after a command fails (default)
|
||||
- `"on-request"`: Approve only when explicitly requested
|
||||
- `"never"`: Never require approval (not recommended)
|
||||
- **`sandboxMode`** (string): Control filesystem access
|
||||
- `"read-only"`: Read-only access
|
||||
- `"workspace-write"`: Allow writes to workspace (default)
|
||||
- `"danger-full-access"`: Full filesystem access (use with caution)
|
||||
- **`codexPath`** (string, optional): Custom path to codex CLI executable
|
||||
- **`cwd`** (string, optional): Working directory for Codex CLI execution
|
||||
- **`fullAuto`** (boolean, optional): Fully automatic mode (equivalent to `--full-auto` flag)
|
||||
- **`dangerouslyBypassApprovalsAndSandbox`** (boolean, optional): Bypass all safety checks (dangerous!)
|
||||
- **`color`** (string, optional): Color handling - `"always"`, `"never"`, or `"auto"`
|
||||
- **`outputLastMessageFile`** (string, optional): Write last agent message to specified file
|
||||
- **`verbose`** (boolean, optional): Enable verbose logging
|
||||
- **`env`** (object, optional): Additional environment variables for Codex CLI
|
||||
|
||||
8. **Command-Specific Settings** (optional):
|
||||
You can override settings for specific Task Master commands:
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"allowNpx": true,
|
||||
"approvalMode": "on-failure",
|
||||
"commandSpecific": {
|
||||
"parse-prd": {
|
||||
"approvalMode": "never",
|
||||
"verbose": true
|
||||
},
|
||||
"expand": {
|
||||
"sandboxMode": "read-only"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
9. **Codebase Features**:
|
||||
The Codex CLI provider is codebase-capable, meaning it can analyze and interact with your project files. Codebase analysis features are automatically enabled when using `codex-cli` as your provider and `enableCodebaseAnalysis` is set to `true` in your global configuration (default).
|
||||
|
||||
10. **Setup Commands**:
|
||||
```bash
|
||||
# Set Codex CLI for main role
|
||||
task-master models --set-main gpt-5-codex --codex-cli
|
||||
|
||||
# Set Codex CLI for fallback role
|
||||
task-master models --set-fallback gpt-5 --codex-cli
|
||||
|
||||
# Verify configuration
|
||||
task-master models
|
||||
```
|
||||
|
||||
11. **Troubleshooting**:
|
||||
|
||||
**"codex: command not found" error:**
|
||||
- Install Codex CLI globally: `npm install -g @openai/codex`
|
||||
- Verify installation: `codex --version`
|
||||
- Alternatively, enable `allowNpx: true` in your codexCli configuration
|
||||
|
||||
**"Not logged in" errors:**
|
||||
- Run `codex login` to authenticate with your ChatGPT account
|
||||
- Verify authentication status: `codex` (opens interactive CLI)
|
||||
|
||||
**"Old version" warnings:**
|
||||
- Check version: `codex --version`
|
||||
- Upgrade: `npm install -g @openai/codex@latest`
|
||||
- Minimum version: 0.42.0, recommended: >= 0.44.0
|
||||
|
||||
**"Model not available" errors:**
|
||||
- Only `gpt-5` and `gpt-5-codex` are available via OAuth subscription
|
||||
- Verify your ChatGPT subscription is active
|
||||
- For other OpenAI models, use the standard `openai` provider with an API key
|
||||
|
||||
**API key not being used:**
|
||||
- API key is only injected when explicitly provided
|
||||
- OAuth authentication is always preferred
|
||||
- If you want to use an API key, ensure `OPENAI_API_KEY` is set in your `.env` file
|
||||
|
||||
12. **Important Notes**:
|
||||
- OAuth subscription required for model access (no API key needed for basic operation)
|
||||
- Limited to OAuth-available models only (`gpt-5` and `gpt-5-codex`)
|
||||
- Pricing information is not available for OAuth models (shows as "Unknown" in cost calculations)
|
||||
- See [Codex CLI Provider Documentation](./providers/codex-cli.md) for more details
|
||||
|
||||
463
docs/examples/codex-cli-usage.md
Normal file
463
docs/examples/codex-cli-usage.md
Normal file
@@ -0,0 +1,463 @@
|
||||
# Codex CLI Provider Usage Examples
|
||||
|
||||
This guide provides practical examples of using Task Master with the Codex CLI provider.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using these examples, ensure you have:
|
||||
|
||||
```bash
|
||||
# 1. Codex CLI installed
|
||||
npm install -g @openai/codex
|
||||
|
||||
# 2. Authenticated with ChatGPT
|
||||
codex login
|
||||
|
||||
# 3. Codex CLI configured as your provider
|
||||
task-master models --set-main gpt-5-codex --codex-cli
|
||||
```
|
||||
|
||||
## Example 1: Basic Task Creation
|
||||
|
||||
Use Codex CLI to create tasks from a simple description:
|
||||
|
||||
```bash
|
||||
# Add a task with AI-powered enhancement
|
||||
task-master add-task --prompt="Implement user authentication with JWT" --research
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. Task Master sends your prompt to GPT-5-Codex via the CLI
|
||||
2. The AI analyzes your request and generates a detailed task
|
||||
3. The task is added to your `.taskmaster/tasks/tasks.json`
|
||||
4. OAuth credentials are automatically used (no API key needed)
|
||||
|
||||
## Example 2: Parsing a Product Requirements Document
|
||||
|
||||
Create a comprehensive task list from a PRD:
|
||||
|
||||
```bash
|
||||
# Create your PRD
|
||||
cat > my-feature.txt <<EOF
|
||||
# User Profile Feature
|
||||
|
||||
## Requirements
|
||||
1. Users can view their profile
|
||||
2. Users can edit their information
|
||||
3. Profile pictures can be uploaded
|
||||
4. Email verification required
|
||||
|
||||
## Technical Constraints
|
||||
- Use React for frontend
|
||||
- Node.js/Express backend
|
||||
- PostgreSQL database
|
||||
EOF
|
||||
|
||||
# Parse with Codex CLI
|
||||
task-master parse-prd my-feature.txt --num-tasks 12
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. GPT-5-Codex reads and analyzes your PRD
|
||||
2. Generates structured tasks with dependencies
|
||||
3. Creates subtasks for complex items
|
||||
4. Saves everything to `.taskmaster/tasks/`
|
||||
|
||||
## Example 3: Expanding Tasks with Research
|
||||
|
||||
Break down a complex task into detailed subtasks:
|
||||
|
||||
```bash
|
||||
# First, show your current tasks
|
||||
task-master list
|
||||
|
||||
# Expand a specific task (e.g., task 1.2)
|
||||
task-master expand --id=1.2 --research --force
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. Codex CLI uses GPT-5 for research-level analysis
|
||||
2. Breaks down the task into logical subtasks
|
||||
3. Adds implementation details and test strategies
|
||||
4. Updates the task with dependency information
|
||||
|
||||
## Example 4: Analyzing Project Complexity
|
||||
|
||||
Get AI-powered insights into your project's task complexity:
|
||||
|
||||
```bash
|
||||
# Analyze all tasks
|
||||
task-master analyze-complexity --research
|
||||
|
||||
# View the complexity report
|
||||
task-master complexity-report
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. GPT-5 analyzes each task's scope and requirements
|
||||
2. Assigns complexity scores and estimates subtask counts
|
||||
3. Generates a detailed report
|
||||
4. Saves to `.taskmaster/reports/task-complexity-report.json`
|
||||
|
||||
## Example 5: Using Custom Codex CLI Settings
|
||||
|
||||
Configure Codex CLI behavior for different commands:
|
||||
|
||||
```json
|
||||
// In .taskmaster/config.json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5-codex",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
"codexCli": {
|
||||
"allowNpx": true,
|
||||
"approvalMode": "on-failure",
|
||||
"sandboxMode": "workspace-write",
|
||||
"commandSpecific": {
|
||||
"parse-prd": {
|
||||
"verbose": true,
|
||||
"approvalMode": "never"
|
||||
},
|
||||
"expand": {
|
||||
"sandboxMode": "read-only",
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Now parse-prd runs with verbose output and no approvals
|
||||
task-master parse-prd requirements.txt
|
||||
|
||||
# Expand runs with read-only mode
|
||||
task-master expand --id=2.1
|
||||
```
|
||||
|
||||
## Example 6: Workflow - Building a Feature End-to-End
|
||||
|
||||
Complete workflow from PRD to implementation tracking:
|
||||
|
||||
```bash
|
||||
# Step 1: Initialize project
|
||||
task-master init
|
||||
|
||||
# Step 2: Set up Codex CLI
|
||||
task-master models --set-main gpt-5-codex --codex-cli
|
||||
task-master models --set-fallback gpt-5 --codex-cli
|
||||
|
||||
# Step 3: Create PRD
|
||||
cat > feature-prd.txt <<EOF
|
||||
# Authentication System
|
||||
|
||||
Implement a complete authentication system with:
|
||||
- User registration
|
||||
- Email verification
|
||||
- Password reset
|
||||
- Two-factor authentication
|
||||
- Session management
|
||||
EOF
|
||||
|
||||
# Step 4: Parse PRD into tasks
|
||||
task-master parse-prd feature-prd.txt --num-tasks 8
|
||||
|
||||
# Step 5: Analyze complexity
|
||||
task-master analyze-complexity --research
|
||||
|
||||
# Step 6: Expand complex tasks
|
||||
task-master expand --all --research
|
||||
|
||||
# Step 7: Start working
|
||||
task-master next
|
||||
# Shows: Task 1.1: User registration database schema
|
||||
|
||||
# Step 8: Mark completed as you work
|
||||
task-master set-status --id=1.1 --status=done
|
||||
|
||||
# Step 9: Continue to next task
|
||||
task-master next
|
||||
```
|
||||
|
||||
## Example 7: Multi-Role Configuration
|
||||
|
||||
Use Codex CLI for main tasks, Perplexity for research:
|
||||
|
||||
```json
|
||||
// In .taskmaster/config.json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5-codex",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"research": {
|
||||
"provider": "perplexity",
|
||||
"modelId": "sonar-pro",
|
||||
"maxTokens": 8700,
|
||||
"temperature": 0.1
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Main task operations use GPT-5-Codex
|
||||
task-master add-task --prompt="Build REST API endpoint"
|
||||
|
||||
# Research operations use Perplexity
|
||||
task-master analyze-complexity --research
|
||||
|
||||
# Fallback to GPT-5 if needed
|
||||
task-master expand --id=3.2 --force
|
||||
```
|
||||
|
||||
## Example 8: Troubleshooting Common Issues
|
||||
|
||||
### Issue: Codex CLI not found
|
||||
|
||||
```bash
|
||||
# Check if Codex is installed
|
||||
codex --version
|
||||
|
||||
# If not found, install globally
|
||||
npm install -g @openai/codex
|
||||
|
||||
# Or enable npx fallback in config
|
||||
cat >> .taskmaster/config.json <<EOF
|
||||
{
|
||||
"codexCli": {
|
||||
"allowNpx": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
### Issue: Not authenticated
|
||||
|
||||
```bash
|
||||
# Check auth status
|
||||
codex
|
||||
# Use /about command to see auth info
|
||||
|
||||
# Re-authenticate if needed
|
||||
codex login
|
||||
```
|
||||
|
||||
### Issue: Want more verbose output
|
||||
|
||||
```bash
|
||||
# Enable verbose mode in config
|
||||
cat >> .taskmaster/config.json <<EOF
|
||||
{
|
||||
"codexCli": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Or for specific commands
|
||||
task-master parse-prd my-prd.txt
|
||||
# (verbose output shows detailed Codex CLI interactions)
|
||||
```
|
||||
|
||||
## Example 9: CI/CD Integration
|
||||
|
||||
Use Codex CLI in automated workflows:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/task-analysis.yml
|
||||
name: Analyze Task Complexity
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.taskmaster/**'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Task Master
|
||||
run: npm install -g task-master-ai
|
||||
|
||||
- name: Configure Codex CLI
|
||||
run: |
|
||||
npm install -g @openai/codex
|
||||
echo "${{ secrets.OPENAI_CODEX_API_KEY }}" > ~/.codex-auth
|
||||
env:
|
||||
OPENAI_CODEX_API_KEY: ${{ secrets.OPENAI_CODEX_API_KEY }}
|
||||
|
||||
- name: Configure Task Master
|
||||
run: |
|
||||
cat > .taskmaster/config.json <<EOF
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5"
|
||||
}
|
||||
},
|
||||
"codexCli": {
|
||||
"allowNpx": true,
|
||||
"skipGitRepoCheck": true,
|
||||
"approvalMode": "never",
|
||||
"fullAuto": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Analyze Complexity
|
||||
run: task-master analyze-complexity --research
|
||||
|
||||
- name: Upload Report
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: complexity-report
|
||||
path: .taskmaster/reports/task-complexity-report.json
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use OAuth for Development
|
||||
|
||||
```bash
|
||||
# For local development, use OAuth (no API key needed)
|
||||
codex login
|
||||
task-master models --set-main gpt-5-codex --codex-cli
|
||||
```
|
||||
|
||||
### 2. Configure Approval Modes Appropriately
|
||||
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"approvalMode": "on-failure", // Safe default
|
||||
"sandboxMode": "workspace-write" // Restricts to project directory
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Command-Specific Settings
|
||||
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"commandSpecific": {
|
||||
"parse-prd": {
|
||||
"approvalMode": "never", // PRD parsing is safe
|
||||
"verbose": true
|
||||
},
|
||||
"expand": {
|
||||
"approvalMode": "on-request", // More cautious for task expansion
|
||||
"verbose": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Leverage Codebase Analysis
|
||||
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"enableCodebaseAnalysis": true // Let Codex analyze your code
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Handle Errors Gracefully
|
||||
|
||||
```bash
|
||||
# Always configure a fallback model
|
||||
task-master models --set-fallback gpt-5 --codex-cli
|
||||
|
||||
# Or use a different provider as fallback
|
||||
task-master models --set-fallback claude-3-5-sonnet
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [Codex CLI Provider Documentation](../providers/codex-cli.md)
|
||||
- Explore [Configuration Options](../configuration.md#codex-cli-provider)
|
||||
- Check out [Command Reference](../command-reference.md)
|
||||
- Learn about [Task Structure](../task-structure.md)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Daily Development Workflow
|
||||
|
||||
```bash
|
||||
# Morning: Review tasks
|
||||
task-master list
|
||||
|
||||
# Get next task
|
||||
task-master next
|
||||
|
||||
# Work on task...
|
||||
|
||||
# Update task with notes
|
||||
task-master update-subtask --id=2.3 --prompt="Implemented authentication middleware"
|
||||
|
||||
# Mark complete
|
||||
task-master set-status --id=2.3 --status=done
|
||||
|
||||
# Repeat
|
||||
```
|
||||
|
||||
### Pattern: Feature Planning
|
||||
|
||||
```bash
|
||||
# Write feature spec
|
||||
vim new-feature.txt
|
||||
|
||||
# Generate tasks
|
||||
task-master parse-prd new-feature.txt --num-tasks 10
|
||||
|
||||
# Analyze and expand
|
||||
task-master analyze-complexity --research
|
||||
task-master expand --all --research --force
|
||||
|
||||
# Review and adjust
|
||||
task-master list
|
||||
```
|
||||
|
||||
### Pattern: Sprint Planning
|
||||
|
||||
```bash
|
||||
# Parse sprint requirements
|
||||
task-master parse-prd sprint-requirements.txt
|
||||
|
||||
# Analyze complexity
|
||||
task-master analyze-complexity --research
|
||||
|
||||
# View report
|
||||
task-master complexity-report
|
||||
|
||||
# Adjust task estimates based on complexity scores
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).
|
||||
@@ -1,4 +1,4 @@
|
||||
# Available Models as of September 23, 2025
|
||||
# Available Models as of October 5, 2025
|
||||
|
||||
## Main Models
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
|
||||
| claude-code | opus | 0.725 | 0 | 0 |
|
||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
|
||||
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
|
||||
| mcp | mcp-sampling | — | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
@@ -100,6 +102,8 @@
|
||||
| ----------- | -------------------------------------------- | --------- | ---------- | ----------- |
|
||||
| claude-code | opus | 0.725 | 0 | 0 |
|
||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
|
||||
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
|
||||
| mcp | mcp-sampling | — | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
@@ -140,6 +144,8 @@
|
||||
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
|
||||
| claude-code | opus | 0.725 | 0 | 0 |
|
||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
|
||||
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
|
||||
| mcp | mcp-sampling | — | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
|
||||
510
docs/providers/codex-cli.md
Normal file
510
docs/providers/codex-cli.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# Codex CLI Provider
|
||||
|
||||
The `codex-cli` provider integrates Task Master with OpenAI's Codex CLI via the community AI SDK provider [`ai-sdk-provider-codex-cli`](https://github.com/ben-vargas/ai-sdk-provider-codex-cli). It uses your ChatGPT subscription (OAuth) via `codex login`, with optional `OPENAI_CODEX_API_KEY` support.
|
||||
|
||||
## Why Use Codex CLI?
|
||||
|
||||
The primary benefits of using the `codex-cli` provider include:
|
||||
|
||||
- **Use Latest OpenAI Models**: Access to cutting-edge models like GPT-5 and GPT-5-Codex via ChatGPT subscription
|
||||
- **OAuth Authentication**: No API key management needed - authenticate once with `codex login`
|
||||
- **Built-in Tool Execution**: Native support for command execution, file changes, MCP tools, and web search
|
||||
- **Native JSON Schema Support**: Structured output generation without post-processing
|
||||
- **Approval/Sandbox Modes**: Fine-grained control over command execution and filesystem access for safety
|
||||
|
||||
## Quickstart
|
||||
|
||||
Get up and running with Codex CLI in 3 steps:
|
||||
|
||||
```bash
|
||||
# 1. Install Codex CLI globally
|
||||
npm install -g @openai/codex
|
||||
|
||||
# 2. Authenticate with your ChatGPT account
|
||||
codex login
|
||||
|
||||
# 3. Configure Task Master to use Codex CLI
|
||||
task-master models --set-main gpt-5-codex --codex-cli
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js**: >= 18.0.0
|
||||
- **Codex CLI**: >= 0.42.0 (>= 0.44.0 recommended)
|
||||
- **ChatGPT Subscription**: Required for OAuth access (Plus, Pro, Business, Edu, or Enterprise)
|
||||
- **Task Master**: >= 0.27.3 (version with Codex CLI support)
|
||||
|
||||
### Checking Your Versions
|
||||
|
||||
```bash
|
||||
# Check Node.js version
|
||||
node --version
|
||||
|
||||
# Check Codex CLI version
|
||||
codex --version
|
||||
|
||||
# Check Task Master version
|
||||
task-master --version
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Install Codex CLI
|
||||
|
||||
```bash
|
||||
# Install globally via npm
|
||||
npm install -g @openai/codex
|
||||
|
||||
# Verify installation
|
||||
codex --version
|
||||
```
|
||||
|
||||
Expected output: `v0.44.0` or higher
|
||||
|
||||
### Install Task Master (if not already installed)
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g task-master-ai
|
||||
|
||||
# Or install in your project
|
||||
npm install --save-dev task-master-ai
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### OAuth Authentication (Primary Method - Recommended)
|
||||
|
||||
The Codex CLI provider is designed to use OAuth authentication with your ChatGPT subscription:
|
||||
|
||||
```bash
|
||||
# Launch Codex CLI and authenticate
|
||||
codex login
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Open a browser window for OAuth authentication
|
||||
2. Prompt you to log in with your ChatGPT account
|
||||
3. Store authentication credentials locally
|
||||
4. Allow Task Master to automatically use these credentials
|
||||
|
||||
To verify your authentication:
|
||||
```bash
|
||||
# Open interactive Codex CLI
|
||||
codex
|
||||
|
||||
# Use /about command to see auth status
|
||||
/about
|
||||
```
|
||||
|
||||
### Optional: API Key Method
|
||||
|
||||
While OAuth is the primary and recommended method, you can optionally use an OpenAI API key:
|
||||
|
||||
```bash
|
||||
# In your .env file
|
||||
OPENAI_CODEX_API_KEY=sk-your-openai-api-key-here
|
||||
```
|
||||
|
||||
**Important Notes**:
|
||||
- The API key will **only** be injected when explicitly provided
|
||||
- OAuth authentication is always preferred when available
|
||||
- Using an API key doesn't provide access to subscription-only models like GPT-5-Codex
|
||||
- For full OpenAI API access with non-subscription models, consider using the standard `openai` provider instead
|
||||
- `OPENAI_CODEX_API_KEY` is specific to the codex-cli provider to avoid conflicts with the `openai` provider's `OPENAI_API_KEY`
|
||||
|
||||
## Available Models
|
||||
|
||||
The Codex CLI provider supports only models available through ChatGPT subscription:
|
||||
|
||||
| Model ID | Description | Max Input Tokens | Max Output Tokens |
|
||||
|----------|-------------|------------------|-------------------|
|
||||
| `gpt-5` | Latest GPT-5 model | 272K | 128K |
|
||||
| `gpt-5-codex` | GPT-5 optimized for agentic software engineering | 272K | 128K |
|
||||
|
||||
**Note**: These models are only available via OAuth subscription through Codex CLI (ChatGPT Plus, Pro, Business, Edu, or Enterprise plans). For other OpenAI models, use the standard `openai` provider with an API key.
|
||||
|
||||
**Research Capabilities**: Both GPT-5 models support web search tools, making them suitable for the `research` role in addition to `main` and `fallback` roles.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
Add Codex CLI to your `.taskmaster/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5-codex",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration with Codex CLI Settings
|
||||
|
||||
The `codexCli` section allows you to customize Codex CLI behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "codex-cli",
|
||||
"modelId": "gpt-5-codex",
|
||||
"maxTokens": 128000,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
"codexCli": {
|
||||
"allowNpx": true,
|
||||
"skipGitRepoCheck": true,
|
||||
"approvalMode": "on-failure",
|
||||
"sandboxMode": "workspace-write",
|
||||
"verbose": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Codex CLI Settings Reference
|
||||
|
||||
#### Core Settings
|
||||
|
||||
- **`allowNpx`** (boolean, default: `false`)
|
||||
- Allow fallback to `npx @openai/codex` if the CLI is not found on PATH
|
||||
- Useful for CI environments or systems without global npm installations
|
||||
- Example: `"allowNpx": true`
|
||||
|
||||
- **`skipGitRepoCheck`** (boolean, default: `false`)
|
||||
- Skip git repository safety check before execution
|
||||
- Recommended for CI environments or non-repository usage
|
||||
- Example: `"skipGitRepoCheck": true`
|
||||
|
||||
#### Execution Control
|
||||
|
||||
- **`approvalMode`** (string)
|
||||
- Controls when to require user approval for command execution
|
||||
- Options:
|
||||
- `"untrusted"`: Require approval for all commands
|
||||
- `"on-failure"`: Only require approval after a command fails (default)
|
||||
- `"on-request"`: Approve only when explicitly requested
|
||||
- `"never"`: Never require approval (use with caution)
|
||||
- Example: `"approvalMode": "on-failure"`
|
||||
|
||||
- **`sandboxMode`** (string)
|
||||
- Controls filesystem access permissions
|
||||
- Options:
|
||||
- `"read-only"`: Read-only access to filesystem
|
||||
- `"workspace-write"`: Allow writes to workspace directory (default)
|
||||
- `"danger-full-access"`: Full filesystem access (use with extreme caution)
|
||||
- Example: `"sandboxMode": "workspace-write"`
|
||||
|
||||
#### Path and Environment
|
||||
|
||||
- **`codexPath`** (string, optional)
|
||||
- Custom path to Codex CLI executable
|
||||
- Useful when Codex is installed in a non-standard location
|
||||
- Example: `"codexPath": "/usr/local/bin/codex"`
|
||||
|
||||
- **`cwd`** (string, optional)
|
||||
- Working directory for Codex CLI execution
|
||||
- Defaults to current working directory
|
||||
- Example: `"cwd": "/path/to/project"`
|
||||
|
||||
- **`env`** (object, optional)
|
||||
- Additional environment variables for Codex CLI
|
||||
- Example: `"env": { "DEBUG": "true" }`
|
||||
|
||||
#### Advanced Settings
|
||||
|
||||
- **`fullAuto`** (boolean, optional)
|
||||
- Fully automatic mode (equivalent to `--full-auto` flag)
|
||||
- Bypasses most approvals for fully automated workflows
|
||||
- Example: `"fullAuto": true`
|
||||
|
||||
- **`dangerouslyBypassApprovalsAndSandbox`** (boolean, optional)
|
||||
- Bypass all safety checks including approvals and sandbox
|
||||
- **WARNING**: Use with extreme caution - can execute arbitrary code
|
||||
- Example: `"dangerouslyBypassApprovalsAndSandbox": false`
|
||||
|
||||
- **`color`** (string, optional)
|
||||
- Force color handling in Codex CLI output
|
||||
- Options: `"always"`, `"never"`, `"auto"`
|
||||
- Example: `"color": "auto"`
|
||||
|
||||
- **`outputLastMessageFile`** (string, optional)
|
||||
- Write last agent message to specified file
|
||||
- Useful for debugging or logging
|
||||
- Example: `"outputLastMessageFile": "./last-message.txt"`
|
||||
|
||||
- **`verbose`** (boolean, optional)
|
||||
- Enable verbose provider logging
|
||||
- Helpful for debugging issues
|
||||
- Example: `"verbose": true`
|
||||
|
||||
### Command-Specific Settings
|
||||
|
||||
Override settings for specific Task Master commands:
|
||||
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"allowNpx": true,
|
||||
"approvalMode": "on-failure",
|
||||
"commandSpecific": {
|
||||
"parse-prd": {
|
||||
"approvalMode": "never",
|
||||
"verbose": true
|
||||
},
|
||||
"expand": {
|
||||
"sandboxMode": "read-only"
|
||||
},
|
||||
"add-task": {
|
||||
"approvalMode": "untrusted"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Setting Codex CLI Models
|
||||
|
||||
```bash
|
||||
# Set Codex CLI for main role
|
||||
task-master models --set-main gpt-5-codex --codex-cli
|
||||
|
||||
# Set Codex CLI for fallback role
|
||||
task-master models --set-fallback gpt-5 --codex-cli
|
||||
|
||||
# Set Codex CLI for research role
|
||||
task-master models --set-research gpt-5 --codex-cli
|
||||
|
||||
# Verify configuration
|
||||
task-master models
|
||||
```
|
||||
|
||||
### Using Codex CLI with Task Master Commands
|
||||
|
||||
Once configured, use Task Master commands as normal:
|
||||
|
||||
```bash
|
||||
# Parse a PRD with Codex CLI
|
||||
task-master parse-prd my-requirements.txt
|
||||
|
||||
# Analyze project complexity
|
||||
task-master analyze-complexity --research
|
||||
|
||||
# Expand a task into subtasks
|
||||
task-master expand --id=1.2
|
||||
|
||||
# Add a new task with AI assistance
|
||||
task-master add-task --prompt="Implement user authentication" --research
|
||||
```
|
||||
|
||||
The provider will automatically use your OAuth credentials when Codex CLI is configured.
|
||||
|
||||
## Codebase Features
|
||||
|
||||
The Codex CLI provider is **codebase-capable**, meaning it can analyze and interact with your project files. This enables advanced features like:
|
||||
|
||||
- **Code Analysis**: Understanding your project structure and dependencies
|
||||
- **Intelligent Suggestions**: Context-aware task recommendations
|
||||
- **File Operations**: Reading and analyzing project files for better task generation
|
||||
- **Pattern Recognition**: Identifying common patterns and best practices in your codebase
|
||||
|
||||
### Enabling Codebase Analysis
|
||||
|
||||
Codebase analysis is automatically enabled when:
|
||||
1. Your provider is set to `codex-cli`
|
||||
2. `enableCodebaseAnalysis` is `true` in your global configuration (default)
|
||||
|
||||
To verify or configure:
|
||||
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"enableCodebaseAnalysis": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "codex: command not found" Error
|
||||
|
||||
**Symptoms**: Task Master reports that the Codex CLI is not found.
|
||||
|
||||
**Solutions**:
|
||||
1. **Install Codex CLI globally**:
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
```
|
||||
|
||||
2. **Verify installation**:
|
||||
```bash
|
||||
codex --version
|
||||
```
|
||||
|
||||
3. **Alternative: Enable npx fallback**:
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"allowNpx": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### "Not logged in" Errors
|
||||
|
||||
**Symptoms**: Authentication errors when trying to use Codex CLI.
|
||||
|
||||
**Solutions**:
|
||||
1. **Authenticate with OAuth**:
|
||||
```bash
|
||||
codex login
|
||||
```
|
||||
|
||||
2. **Verify authentication status**:
|
||||
```bash
|
||||
codex
|
||||
# Then use /about command
|
||||
```
|
||||
|
||||
3. **Re-authenticate if needed**:
|
||||
```bash
|
||||
# Logout first
|
||||
codex
|
||||
# Use /auth command to change auth method
|
||||
|
||||
# Then login again
|
||||
codex login
|
||||
```
|
||||
|
||||
### "Old version" Warnings
|
||||
|
||||
**Symptoms**: Warnings about Codex CLI version being outdated.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check current version**:
|
||||
```bash
|
||||
codex --version
|
||||
```
|
||||
|
||||
2. **Upgrade to latest version**:
|
||||
```bash
|
||||
npm install -g @openai/codex@latest
|
||||
```
|
||||
|
||||
3. **Verify upgrade**:
|
||||
```bash
|
||||
codex --version
|
||||
```
|
||||
Should show >= 0.44.0
|
||||
|
||||
### "Model not available" Errors
|
||||
|
||||
**Symptoms**: Error indicating the requested model is not available.
|
||||
|
||||
**Causes and Solutions**:
|
||||
|
||||
1. **Using unsupported model**:
|
||||
- Only `gpt-5` and `gpt-5-codex` are available via Codex CLI
|
||||
- For other OpenAI models, use the standard `openai` provider
|
||||
|
||||
2. **Subscription not active**:
|
||||
- Verify your ChatGPT subscription is active
|
||||
- Check subscription status at <https://platform.openai.com>
|
||||
|
||||
3. **Wrong provider selected**:
|
||||
- Verify you're using `--codex-cli` flag when setting models
|
||||
- Check `.taskmaster/config.json` shows `"provider": "codex-cli"`
|
||||
|
||||
### API Key Not Being Used
|
||||
|
||||
**Symptoms**: You've set `OPENAI_CODEX_API_KEY` but it's not being used.
|
||||
|
||||
**Expected Behavior**:
|
||||
- OAuth authentication is always preferred
|
||||
- API key is only injected when explicitly provided
|
||||
- API key doesn't grant access to subscription-only models
|
||||
|
||||
**Solutions**:
|
||||
1. **Verify OAuth is working**:
|
||||
```bash
|
||||
codex
|
||||
# Check /about for auth status
|
||||
```
|
||||
|
||||
2. **If you want to force API key usage**:
|
||||
- This is not recommended with Codex CLI
|
||||
- Consider using the standard `openai` provider instead
|
||||
|
||||
3. **Verify .env file is being loaded**:
|
||||
```bash
|
||||
# Check if .env exists in project root
|
||||
ls -la .env
|
||||
|
||||
# Verify OPENAI_CODEX_API_KEY is set
|
||||
grep OPENAI_CODEX_API_KEY .env
|
||||
```
|
||||
|
||||
### Approval/Sandbox Issues
|
||||
|
||||
**Symptoms**: Commands are blocked or filesystem access is denied.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Adjust approval mode**:
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"approvalMode": "on-request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Adjust sandbox mode**:
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"sandboxMode": "workspace-write"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **For fully automated workflows** (use cautiously):
|
||||
```json
|
||||
{
|
||||
"codexCli": {
|
||||
"fullAuto": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **OAuth subscription required**: No API key needed for basic operation, but requires active ChatGPT subscription
|
||||
- **Limited model selection**: Only `gpt-5` and `gpt-5-codex` available via OAuth
|
||||
- **Pricing information**: Not available for OAuth models (shows as "Unknown" in cost calculations)
|
||||
- **No automatic dependency**: The `@openai/codex` package is not added to Task Master's dependencies - install it globally or enable `allowNpx`
|
||||
- **Codebase analysis**: Automatically enabled when using `codex-cli` provider
|
||||
- **Safety first**: Default settings prioritize safety with `approvalMode: "on-failure"` and `sandboxMode: "workspace-write"`
|
||||
|
||||
## See Also
|
||||
|
||||
- [Configuration Guide](../configuration.md#codex-cli-provider) - Complete Codex CLI configuration reference
|
||||
- [Command Reference](../command-reference.md) - Using `--codex-cli` flag with commands
|
||||
- [Gemini CLI Provider](./gemini-cli.md) - Similar CLI-based provider for Google Gemini
|
||||
- [Claude Code Integration](../claude-code-integration.md) - Another CLI-based provider
|
||||
- [ai-sdk-provider-codex-cli](https://github.com/ben-vargas/ai-sdk-provider-codex-cli) - Source code for the provider package
|
||||
@@ -69,11 +69,29 @@ export function resolveTasksPath(args, log = silentLogger) {
|
||||
|
||||
// Use core findTasksPath with explicit path and normalized projectRoot context
|
||||
if (projectRoot) {
|
||||
return coreFindTasksPath(explicitPath, { projectRoot }, log);
|
||||
const foundPath = coreFindTasksPath(explicitPath, { projectRoot }, log);
|
||||
// If core function returns null and no explicit path was provided,
|
||||
// construct the expected default path as documented
|
||||
if (foundPath === null && !explicitPath) {
|
||||
const defaultPath = path.join(
|
||||
projectRoot,
|
||||
'.taskmaster',
|
||||
'tasks',
|
||||
'tasks.json'
|
||||
);
|
||||
log?.info?.(
|
||||
`Core findTasksPath returned null, using default path: ${defaultPath}`
|
||||
);
|
||||
return defaultPath;
|
||||
}
|
||||
return foundPath;
|
||||
}
|
||||
|
||||
// Fallback to core function without projectRoot context
|
||||
return coreFindTasksPath(explicitPath, null, log);
|
||||
const foundPath = coreFindTasksPath(explicitPath, null, log);
|
||||
// Note: When no projectRoot is available, we can't construct a default path
|
||||
// so we return null and let the calling code handle the error
|
||||
return foundPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,13 +75,50 @@ function generateExampleFromSchema(schema) {
|
||||
return result;
|
||||
|
||||
case 'ZodString':
|
||||
return 'string';
|
||||
// Check for min/max length constraints
|
||||
if (def.checks) {
|
||||
const minCheck = def.checks.find((c) => c.kind === 'min');
|
||||
const maxCheck = def.checks.find((c) => c.kind === 'max');
|
||||
if (minCheck && maxCheck) {
|
||||
return (
|
||||
'<string between ' +
|
||||
minCheck.value +
|
||||
'-' +
|
||||
maxCheck.value +
|
||||
' characters>'
|
||||
);
|
||||
} else if (minCheck) {
|
||||
return '<string with at least ' + minCheck.value + ' characters>';
|
||||
} else if (maxCheck) {
|
||||
return '<string up to ' + maxCheck.value + ' characters>';
|
||||
}
|
||||
}
|
||||
return '<string>';
|
||||
|
||||
case 'ZodNumber':
|
||||
return 0;
|
||||
// Check for int, positive, min/max constraints
|
||||
if (def.checks) {
|
||||
const intCheck = def.checks.find((c) => c.kind === 'int');
|
||||
const minCheck = def.checks.find((c) => c.kind === 'min');
|
||||
const maxCheck = def.checks.find((c) => c.kind === 'max');
|
||||
|
||||
if (intCheck && minCheck && minCheck.value > 0) {
|
||||
return '<positive integer>';
|
||||
} else if (intCheck) {
|
||||
return '<integer>';
|
||||
} else if (minCheck || maxCheck) {
|
||||
return (
|
||||
'<number' +
|
||||
(minCheck ? ' >= ' + minCheck.value : '') +
|
||||
(maxCheck ? ' <= ' + maxCheck.value : '') +
|
||||
'>'
|
||||
);
|
||||
}
|
||||
}
|
||||
return '<number>';
|
||||
|
||||
case 'ZodBoolean':
|
||||
return false;
|
||||
return '<boolean>';
|
||||
|
||||
case 'ZodArray':
|
||||
const elementExample = generateExampleFromSchema(def.type);
|
||||
|
||||
49
output.txt
Normal file
49
output.txt
Normal file
File diff suppressed because one or more lines are too long
6742
package-lock.json
generated
6742
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.27.2",
|
||||
"version": "0.28.0-rc.1",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -71,6 +71,7 @@
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"ai": "^5.0.51",
|
||||
"ai-sdk-provider-claude-code": "^1.1.4",
|
||||
"ai-sdk-provider-codex-cli": "^0.3.0",
|
||||
"ai-sdk-provider-gemini-cli": "^1.1.1",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
|
||||
@@ -33,6 +33,9 @@ export class TaskEntity implements Task {
|
||||
tags?: string[];
|
||||
assignee?: string;
|
||||
complexity?: Task['complexity'];
|
||||
recommendedSubtasks?: number;
|
||||
expansionPrompt?: string;
|
||||
complexityReasoning?: string;
|
||||
|
||||
constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) {
|
||||
this.validate(data);
|
||||
@@ -62,6 +65,9 @@ export class TaskEntity implements Task {
|
||||
this.tags = data.tags;
|
||||
this.assignee = data.assignee;
|
||||
this.complexity = data.complexity;
|
||||
this.recommendedSubtasks = data.recommendedSubtasks;
|
||||
this.expansionPrompt = data.expansionPrompt;
|
||||
this.complexityReasoning = data.complexityReasoning;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +252,10 @@ export class TaskEntity implements Task {
|
||||
actualEffort: this.actualEffort,
|
||||
tags: this.tags,
|
||||
assignee: this.assignee,
|
||||
complexity: this.complexity
|
||||
complexity: this.complexity,
|
||||
recommendedSubtasks: this.recommendedSubtasks,
|
||||
expansionPrompt: this.expansionPrompt,
|
||||
complexityReasoning: this.complexityReasoning
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -61,3 +61,12 @@ export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
|
||||
|
||||
// Re-export executors
|
||||
export * from './executors/index.js';
|
||||
|
||||
// Re-export reports
|
||||
export {
|
||||
ComplexityReportManager,
|
||||
type ComplexityReport,
|
||||
type ComplexityReportMetadata,
|
||||
type ComplexityAnalysis,
|
||||
type TaskComplexityData
|
||||
} from './reports/index.js';
|
||||
|
||||
185
packages/tm-core/src/reports/complexity-report-manager.ts
Normal file
185
packages/tm-core/src/reports/complexity-report-manager.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @fileoverview ComplexityReportManager - Handles loading and managing complexity analysis reports
|
||||
* Follows the same pattern as ConfigManager and AuthManager
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import type {
|
||||
ComplexityReport,
|
||||
ComplexityAnalysis,
|
||||
TaskComplexityData
|
||||
} from './types.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
|
||||
const logger = getLogger('ComplexityReportManager');
|
||||
|
||||
/**
|
||||
* Manages complexity analysis reports
|
||||
* Handles loading, caching, and providing complexity data for tasks
|
||||
*/
|
||||
export class ComplexityReportManager {
|
||||
private projectRoot: string;
|
||||
private reportCache: Map<string, ComplexityReport> = new Map();
|
||||
|
||||
constructor(projectRoot: string) {
|
||||
this.projectRoot = projectRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the complexity report file for a given tag
|
||||
*/
|
||||
private getReportPath(tag?: string): string {
|
||||
const reportsDir = path.join(this.projectRoot, '.taskmaster', 'reports');
|
||||
const tagSuffix = tag && tag !== 'master' ? `_${tag}` : '';
|
||||
return path.join(reportsDir, `task-complexity-report${tagSuffix}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load complexity report for a given tag
|
||||
* Results are cached to avoid repeated file reads
|
||||
*/
|
||||
async loadReport(tag?: string): Promise<ComplexityReport | null> {
|
||||
const resolvedTag = tag || 'master';
|
||||
const cacheKey = resolvedTag;
|
||||
|
||||
// Check cache first
|
||||
if (this.reportCache.has(cacheKey)) {
|
||||
return this.reportCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const reportPath = this.getReportPath(tag);
|
||||
|
||||
try {
|
||||
// Check if file exists
|
||||
await fs.access(reportPath);
|
||||
|
||||
// Read and parse the report
|
||||
const content = await fs.readFile(reportPath, 'utf-8');
|
||||
const report = JSON.parse(content) as ComplexityReport;
|
||||
|
||||
// Validate basic structure
|
||||
if (!report.meta || !Array.isArray(report.complexityAnalysis)) {
|
||||
logger.warn(
|
||||
`Invalid complexity report structure at ${reportPath}, ignoring`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cache the report
|
||||
this.reportCache.set(cacheKey, report);
|
||||
|
||||
logger.debug(
|
||||
`Loaded complexity report for tag '${resolvedTag}' with ${report.complexityAnalysis.length} analyses`
|
||||
);
|
||||
|
||||
return report;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// File doesn't exist - this is normal, not all projects have complexity reports
|
||||
logger.debug(`No complexity report found for tag '${resolvedTag}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Other errors (parsing, permissions, etc.)
|
||||
logger.warn(
|
||||
`Failed to load complexity report for tag '${resolvedTag}': ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity data for a specific task ID
|
||||
*/
|
||||
async getComplexityForTask(
|
||||
taskId: string | number,
|
||||
tag?: string
|
||||
): Promise<TaskComplexityData | null> {
|
||||
const report = await this.loadReport(tag);
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the analysis for this task
|
||||
const analysis = report.complexityAnalysis.find(
|
||||
(a) => String(a.taskId) === String(taskId)
|
||||
);
|
||||
|
||||
if (!analysis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to TaskComplexityData format
|
||||
return {
|
||||
complexityScore: analysis.complexityScore,
|
||||
recommendedSubtasks: analysis.recommendedSubtasks,
|
||||
expansionPrompt: analysis.expansionPrompt,
|
||||
complexityReasoning: analysis.complexityReasoning
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity data for multiple tasks at once
|
||||
* More efficient than calling getComplexityForTask multiple times
|
||||
*/
|
||||
async getComplexityForTasks(
|
||||
taskIds: (string | number)[],
|
||||
tag?: string
|
||||
): Promise<Map<string, TaskComplexityData>> {
|
||||
const result = new Map<string, TaskComplexityData>();
|
||||
const report = await this.loadReport(tag);
|
||||
|
||||
if (!report) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create a map for fast lookups
|
||||
const analysisMap = new Map<string, ComplexityAnalysis>();
|
||||
report.complexityAnalysis.forEach((analysis) => {
|
||||
analysisMap.set(String(analysis.taskId), analysis);
|
||||
});
|
||||
|
||||
// Map each task ID to its complexity data
|
||||
taskIds.forEach((taskId) => {
|
||||
const analysis = analysisMap.get(String(taskId));
|
||||
if (analysis) {
|
||||
result.set(String(taskId), {
|
||||
complexityScore: analysis.complexityScore,
|
||||
recommendedSubtasks: analysis.recommendedSubtasks,
|
||||
expansionPrompt: analysis.expansionPrompt,
|
||||
complexityReasoning: analysis.complexityReasoning
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the report cache
|
||||
* @param tag - Specific tag to clear, or undefined to clear all cached reports
|
||||
* Useful when reports are regenerated or modified externally
|
||||
*/
|
||||
clearCache(tag?: string): void {
|
||||
if (tag) {
|
||||
this.reportCache.delete(tag);
|
||||
} else {
|
||||
// Clear all cached reports
|
||||
this.reportCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a complexity report exists for a tag
|
||||
*/
|
||||
async hasReport(tag?: string): Promise<boolean> {
|
||||
const reportPath = this.getReportPath(tag);
|
||||
try {
|
||||
await fs.access(reportPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/tm-core/src/reports/index.ts
Normal file
11
packages/tm-core/src/reports/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @fileoverview Reports module exports
|
||||
*/
|
||||
|
||||
export { ComplexityReportManager } from './complexity-report-manager.js';
|
||||
export type {
|
||||
ComplexityReport,
|
||||
ComplexityReportMetadata,
|
||||
ComplexityAnalysis,
|
||||
TaskComplexityData
|
||||
} from './types.js';
|
||||
65
packages/tm-core/src/reports/types.ts
Normal file
65
packages/tm-core/src/reports/types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @fileoverview Type definitions for complexity analysis reports
|
||||
*/
|
||||
|
||||
/**
|
||||
* Analysis result for a single task
|
||||
*/
|
||||
export interface ComplexityAnalysis {
|
||||
/** Task ID being analyzed */
|
||||
taskId: string | number;
|
||||
/** Task title */
|
||||
taskTitle: string;
|
||||
/** Complexity score (1-10 scale) */
|
||||
complexityScore: number;
|
||||
/** Recommended number of subtasks */
|
||||
recommendedSubtasks: number;
|
||||
/** AI-generated prompt for task expansion */
|
||||
expansionPrompt: string;
|
||||
/** Reasoning behind the complexity assessment */
|
||||
complexityReasoning: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata about the complexity report
|
||||
*/
|
||||
export interface ComplexityReportMetadata {
|
||||
/** When the report was generated */
|
||||
generatedAt: string;
|
||||
/** Number of tasks analyzed in this run */
|
||||
tasksAnalyzed: number;
|
||||
/** Total number of tasks in the file */
|
||||
totalTasks?: number;
|
||||
/** Total analyses in the report (across all runs) */
|
||||
analysisCount?: number;
|
||||
/** Complexity threshold score used */
|
||||
thresholdScore: number;
|
||||
/** Project name */
|
||||
projectName?: string;
|
||||
/** Whether research mode was used */
|
||||
usedResearch: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete complexity analysis report
|
||||
*/
|
||||
export interface ComplexityReport {
|
||||
/** Report metadata */
|
||||
meta: ComplexityReportMetadata;
|
||||
/** Array of complexity analyses */
|
||||
complexityAnalysis: ComplexityAnalysis[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complexity data to be attached to a Task
|
||||
*/
|
||||
export interface TaskComplexityData {
|
||||
/** Complexity score (1-10 scale) */
|
||||
complexityScore?: number;
|
||||
/** Recommended number of subtasks */
|
||||
recommendedSubtasks?: number;
|
||||
/** AI-generated expansion prompt */
|
||||
expansionPrompt?: string;
|
||||
/** Reasoning behind the assessment */
|
||||
complexityReasoning?: string;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export class SupabaseTaskRepository {
|
||||
TaskUpdateSchema.parse(updates);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errorMessages = error.errors
|
||||
const errorMessages = error.issues
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||
|
||||
@@ -135,15 +135,28 @@ export class TaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single task by ID
|
||||
* Get a single task by ID - delegates to storage layer
|
||||
*/
|
||||
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||
const result = await this.getTaskList({
|
||||
tag,
|
||||
includeSubtasks: true
|
||||
});
|
||||
// Use provided tag or get active tag
|
||||
const activeTag = tag || this.getActiveTag();
|
||||
|
||||
return result.tasks.find((t) => t.id === taskId) || null;
|
||||
try {
|
||||
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
||||
return await this.storage.loadTask(String(taskId), activeTag);
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
`Failed to get task ${taskId}`,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
{
|
||||
operation: 'getTask',
|
||||
resource: 'task',
|
||||
taskId: String(taskId),
|
||||
tag: activeTag
|
||||
},
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,16 +397,6 @@ export class TaskService {
|
||||
}
|
||||
}
|
||||
|
||||
// Complexity filter
|
||||
if (filter.complexity) {
|
||||
const complexities = Array.isArray(filter.complexity)
|
||||
? filter.complexity
|
||||
: [filter.complexity];
|
||||
if (!task.complexity || !complexities.includes(task.complexity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (filter.search) {
|
||||
const searchLower = filter.search.toLowerCase();
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import { FormatHandler } from './format-handler.js';
|
||||
import { FileOperations } from './file-operations.js';
|
||||
import { PathResolver } from './path-resolver.js';
|
||||
import { ComplexityReportManager } from '../../reports/complexity-report-manager.js';
|
||||
|
||||
/**
|
||||
* File-based storage implementation using a single tasks.json file with separated concerns
|
||||
@@ -19,11 +20,13 @@ export class FileStorage implements IStorage {
|
||||
private formatHandler: FormatHandler;
|
||||
private fileOps: FileOperations;
|
||||
private pathResolver: PathResolver;
|
||||
private complexityManager: ComplexityReportManager;
|
||||
|
||||
constructor(projectPath: string) {
|
||||
this.formatHandler = new FormatHandler();
|
||||
this.fileOps = new FileOperations();
|
||||
this.pathResolver = new PathResolver(projectPath);
|
||||
this.complexityManager = new ComplexityReportManager(projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +90,7 @@ export class FileStorage implements IStorage {
|
||||
|
||||
/**
|
||||
* Load tasks from the single tasks.json file for a specific tag
|
||||
* Enriches tasks with complexity data from the complexity report
|
||||
*/
|
||||
async loadTasks(tag?: string): Promise<Task[]> {
|
||||
const filePath = this.pathResolver.getTasksPath();
|
||||
@@ -94,7 +98,10 @@ export class FileStorage implements IStorage {
|
||||
|
||||
try {
|
||||
const rawData = await this.fileOps.readJson(filePath);
|
||||
return this.formatHandler.extractTasks(rawData, resolvedTag);
|
||||
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
|
||||
|
||||
// Enrich tasks with complexity data
|
||||
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []; // File doesn't exist, return empty array
|
||||
@@ -105,9 +112,65 @@ export class FileStorage implements IStorage {
|
||||
|
||||
/**
|
||||
* Load a single task by ID from the tasks.json file
|
||||
* Handles both regular tasks and subtasks (with dotted notation like "1.2")
|
||||
*/
|
||||
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
|
||||
// Check if this is a subtask (contains a dot)
|
||||
if (taskId.includes('.')) {
|
||||
const [parentId, subtaskId] = taskId.split('.');
|
||||
const parentTask = tasks.find((t) => String(t.id) === parentId);
|
||||
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subtask = parentTask.subtasks.find(
|
||||
(st) => String(st.id) === subtaskId
|
||||
);
|
||||
if (!subtask) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toFullSubId = (maybeDotId: string | number): string => {
|
||||
const depId = String(maybeDotId);
|
||||
return depId.includes('.') ? depId : `${parentTask.id}.${depId}`;
|
||||
};
|
||||
const resolvedDependencies =
|
||||
subtask.dependencies?.map((dep) => toFullSubId(dep)) ?? [];
|
||||
|
||||
// Return a Task-like object for the subtask with the full dotted ID
|
||||
// Following the same pattern as findTaskById in utils.js
|
||||
const subtaskResult = {
|
||||
...subtask,
|
||||
id: taskId, // Use the full dotted ID
|
||||
title: subtask.title || `Subtask ${subtaskId}`,
|
||||
description: subtask.description || '',
|
||||
status: subtask.status || 'pending',
|
||||
priority: subtask.priority || parentTask.priority || 'medium',
|
||||
dependencies: resolvedDependencies,
|
||||
details: subtask.details || '',
|
||||
testStrategy: subtask.testStrategy || '',
|
||||
subtasks: [],
|
||||
tags: parentTask.tags || [],
|
||||
assignee: subtask.assignee || parentTask.assignee,
|
||||
complexity: subtask.complexity || parentTask.complexity,
|
||||
createdAt: subtask.createdAt || parentTask.createdAt,
|
||||
updatedAt: subtask.updatedAt || parentTask.updatedAt,
|
||||
// Add reference to parent task for context (like utils.js does)
|
||||
parentTask: {
|
||||
id: parentTask.id,
|
||||
title: parentTask.title,
|
||||
status: parentTask.status
|
||||
},
|
||||
isSubtask: true
|
||||
};
|
||||
|
||||
return subtaskResult;
|
||||
}
|
||||
|
||||
// Handle regular task lookup
|
||||
return tasks.find((task) => String(task.id) === String(taskId)) || null;
|
||||
}
|
||||
|
||||
@@ -540,6 +603,46 @@ export class FileStorage implements IStorage {
|
||||
|
||||
await this.saveTasks(tasks, targetTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich tasks with complexity data from the complexity report
|
||||
* Private helper method called by loadTasks()
|
||||
*/
|
||||
private async enrichTasksWithComplexity(
|
||||
tasks: Task[],
|
||||
tag: string
|
||||
): Promise<Task[]> {
|
||||
// Get all task IDs for bulk lookup
|
||||
const taskIds = tasks.map((t) => t.id);
|
||||
|
||||
// Load complexity data for all tasks at once (more efficient)
|
||||
const complexityMap = await this.complexityManager.getComplexityForTasks(
|
||||
taskIds,
|
||||
tag
|
||||
);
|
||||
|
||||
// If no complexity data found, return tasks as-is
|
||||
if (complexityMap.size === 0) {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
// Enrich each task with its complexity data
|
||||
return tasks.map((task) => {
|
||||
const complexityData = complexityMap.get(String(task.id));
|
||||
if (!complexityData) {
|
||||
return task;
|
||||
}
|
||||
|
||||
// Merge complexity data into the task
|
||||
return {
|
||||
...task,
|
||||
complexity: complexityData.complexityScore,
|
||||
recommendedSubtasks: complexityData.recommendedSubtasks,
|
||||
expansionPrompt: complexityData.expansionPrompt,
|
||||
complexityReasoning: complexityData.complexityReasoning
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export as default for convenience
|
||||
|
||||
@@ -72,7 +72,13 @@ export interface Task {
|
||||
actualEffort?: number;
|
||||
tags?: string[];
|
||||
assignee?: string;
|
||||
complexity?: TaskComplexity;
|
||||
|
||||
// Complexity analysis (from complexity report)
|
||||
// Can be either enum ('simple' | 'moderate' | 'complex' | 'very-complex') or numeric score (1-10)
|
||||
complexity?: TaskComplexity | number;
|
||||
recommendedSubtasks?: number;
|
||||
expansionPrompt?: string;
|
||||
complexityReasoning?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +151,6 @@ export interface TaskFilter {
|
||||
hasSubtasks?: boolean;
|
||||
search?: string;
|
||||
assignee?: string;
|
||||
complexity?: TaskComplexity | TaskComplexity[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
AzureProvider,
|
||||
BedrockAIProvider,
|
||||
ClaudeCodeProvider,
|
||||
CodexCliProvider,
|
||||
GeminiCliProvider,
|
||||
GoogleAIProvider,
|
||||
GrokCliProvider,
|
||||
@@ -70,6 +71,7 @@ const PROVIDERS = {
|
||||
azure: new AzureProvider(),
|
||||
vertex: new VertexAIProvider(),
|
||||
'claude-code': new ClaudeCodeProvider(),
|
||||
'codex-cli': new CodexCliProvider(),
|
||||
'gemini-cli': new GeminiCliProvider(),
|
||||
'grok-cli': new GrokCliProvider()
|
||||
};
|
||||
@@ -93,31 +95,55 @@ function _getProvider(providerName) {
|
||||
|
||||
// Helper function to get cost for a specific model
|
||||
function _getCostForModel(providerName, modelId) {
|
||||
const DEFAULT_COST = { inputCost: 0, outputCost: 0, currency: 'USD' };
|
||||
const DEFAULT_COST = {
|
||||
inputCost: 0,
|
||||
outputCost: 0,
|
||||
currency: 'USD',
|
||||
isUnknown: false
|
||||
};
|
||||
|
||||
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
||||
log(
|
||||
'warn',
|
||||
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
|
||||
);
|
||||
return DEFAULT_COST;
|
||||
return { ...DEFAULT_COST, isUnknown: true };
|
||||
}
|
||||
|
||||
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
|
||||
|
||||
if (!modelData?.cost_per_1m_tokens) {
|
||||
if (!modelData) {
|
||||
log(
|
||||
'debug',
|
||||
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
|
||||
`Model "${modelId}" not found under provider "${providerName}". Assuming unknown cost.`
|
||||
);
|
||||
return DEFAULT_COST;
|
||||
return { ...DEFAULT_COST, isUnknown: true };
|
||||
}
|
||||
|
||||
// Check if cost_per_1m_tokens is explicitly null (unknown pricing)
|
||||
if (modelData.cost_per_1m_tokens === null) {
|
||||
log(
|
||||
'debug',
|
||||
`Cost data is null for model "${modelId}" under provider "${providerName}". Pricing unknown.`
|
||||
);
|
||||
return { ...DEFAULT_COST, isUnknown: true };
|
||||
}
|
||||
|
||||
// Check if cost_per_1m_tokens is missing/undefined (also unknown)
|
||||
if (modelData.cost_per_1m_tokens === undefined) {
|
||||
log(
|
||||
'debug',
|
||||
`Cost data not found for model "${modelId}" under provider "${providerName}". Pricing unknown.`
|
||||
);
|
||||
return { ...DEFAULT_COST, isUnknown: true };
|
||||
}
|
||||
|
||||
const costs = modelData.cost_per_1m_tokens;
|
||||
return {
|
||||
inputCost: costs.input || 0,
|
||||
outputCost: costs.output || 0,
|
||||
currency: costs.currency || 'USD'
|
||||
currency: costs.currency || 'USD',
|
||||
isUnknown: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -867,8 +893,8 @@ async function logAiUsage({
|
||||
const timestamp = new Date().toISOString();
|
||||
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
|
||||
|
||||
// Destructure currency along with costs
|
||||
const { inputCost, outputCost, currency } = _getCostForModel(
|
||||
// Destructure currency along with costs and unknown flag
|
||||
const { inputCost, outputCost, currency, isUnknown } = _getCostForModel(
|
||||
providerName,
|
||||
modelId
|
||||
);
|
||||
@@ -890,7 +916,8 @@ async function logAiUsage({
|
||||
outputTokens: outputTokens || 0,
|
||||
totalTokens,
|
||||
totalCost,
|
||||
currency // Add currency to the telemetry data
|
||||
currency, // Add currency to the telemetry data
|
||||
isUnknownCost: isUnknown // Flag to indicate if pricing is unknown
|
||||
};
|
||||
|
||||
if (getDebugFlag()) {
|
||||
|
||||
@@ -3586,6 +3586,10 @@ ${result.result}
|
||||
'--gemini-cli',
|
||||
'Allow setting a Gemini CLI model ID (use with --set-*)'
|
||||
)
|
||||
.option(
|
||||
'--codex-cli',
|
||||
'Allow setting a Codex CLI model ID (use with --set-*)'
|
||||
)
|
||||
.addHelpText(
|
||||
'after',
|
||||
`
|
||||
@@ -3601,6 +3605,7 @@ Examples:
|
||||
$ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
|
||||
$ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
|
||||
$ task-master models --set-main gemini-2.5-pro --gemini-cli # Set Gemini CLI model for main role
|
||||
$ task-master models --set-main gpt-5-codex --codex-cli # Set Codex CLI model for main role
|
||||
$ task-master models --setup # Run interactive setup`
|
||||
)
|
||||
.action(async (options) => {
|
||||
@@ -3617,12 +3622,13 @@ Examples:
|
||||
options.ollama,
|
||||
options.bedrock,
|
||||
options.claudeCode,
|
||||
options.geminiCli
|
||||
options.geminiCli,
|
||||
options.codexCli
|
||||
].filter(Boolean).length;
|
||||
if (providerFlags > 1) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli) simultaneously.'
|
||||
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli, --codex-cli) simultaneously.'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -3668,7 +3674,9 @@ Examples:
|
||||
? 'claude-code'
|
||||
: options.geminiCli
|
||||
? 'gemini-cli'
|
||||
: undefined
|
||||
: options.codexCli
|
||||
? 'codex-cli'
|
||||
: undefined
|
||||
});
|
||||
if (result.success) {
|
||||
console.log(chalk.green(`✅ ${result.data.message}`));
|
||||
@@ -3694,7 +3702,9 @@ Examples:
|
||||
? 'claude-code'
|
||||
: options.geminiCli
|
||||
? 'gemini-cli'
|
||||
: undefined
|
||||
: options.codexCli
|
||||
? 'codex-cli'
|
||||
: undefined
|
||||
});
|
||||
if (result.success) {
|
||||
console.log(chalk.green(`✅ ${result.data.message}`));
|
||||
@@ -3722,7 +3732,9 @@ Examples:
|
||||
? 'claude-code'
|
||||
: options.geminiCli
|
||||
? 'gemini-cli'
|
||||
: undefined
|
||||
: options.codexCli
|
||||
? 'codex-cli'
|
||||
: undefined
|
||||
});
|
||||
if (result.success) {
|
||||
console.log(chalk.green(`✅ ${result.data.message}`));
|
||||
|
||||
@@ -58,6 +58,7 @@ const DEFAULTS = {
|
||||
enableCodebaseAnalysis: true
|
||||
},
|
||||
claudeCode: {},
|
||||
codexCli: {},
|
||||
grokCli: {
|
||||
timeout: 120000,
|
||||
workingDirectory: null,
|
||||
@@ -138,6 +139,7 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
||||
},
|
||||
global: { ...defaults.global, ...parsedConfig?.global },
|
||||
claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode },
|
||||
codexCli: { ...defaults.codexCli, ...parsedConfig?.codexCli },
|
||||
grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli }
|
||||
};
|
||||
configSource = `file (${configPath})`; // Update source info
|
||||
@@ -184,6 +186,9 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
||||
if (config.claudeCode && !isEmpty(config.claudeCode)) {
|
||||
config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
|
||||
}
|
||||
if (config.codexCli && !isEmpty(config.codexCli)) {
|
||||
config.codexCli = validateCodexCliSettings(config.codexCli);
|
||||
}
|
||||
} catch (error) {
|
||||
// Use console.error for actual errors during parsing
|
||||
console.error(
|
||||
@@ -366,6 +371,57 @@ function validateClaudeCodeSettings(settings) {
|
||||
return validatedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Codex CLI provider custom settings
|
||||
* Mirrors the ai-sdk-provider-codex-cli options
|
||||
* @param {object} settings The settings to validate
|
||||
* @returns {object} The validated settings
|
||||
*/
|
||||
function validateCodexCliSettings(settings) {
|
||||
const BaseSettingsSchema = z.object({
|
||||
codexPath: z.string().optional(),
|
||||
cwd: z.string().optional(),
|
||||
approvalMode: z
|
||||
.enum(['untrusted', 'on-failure', 'on-request', 'never'])
|
||||
.optional(),
|
||||
sandboxMode: z
|
||||
.enum(['read-only', 'workspace-write', 'danger-full-access'])
|
||||
.optional(),
|
||||
fullAuto: z.boolean().optional(),
|
||||
dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(),
|
||||
skipGitRepoCheck: z.boolean().optional(),
|
||||
color: z.enum(['always', 'never', 'auto']).optional(),
|
||||
allowNpx: z.boolean().optional(),
|
||||
outputLastMessageFile: z.string().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
verbose: z.boolean().optional(),
|
||||
logger: z.union([z.object({}).passthrough(), z.literal(false)]).optional()
|
||||
});
|
||||
|
||||
const CommandSpecificSchema = z
|
||||
.record(z.string(), BaseSettingsSchema)
|
||||
.refine(
|
||||
(obj) =>
|
||||
Object.keys(obj || {}).every((k) => AI_COMMAND_NAMES.includes(k)),
|
||||
{ message: 'Invalid command name in commandSpecific' }
|
||||
);
|
||||
|
||||
const SettingsSchema = BaseSettingsSchema.extend({
|
||||
commandSpecific: CommandSpecificSchema.optional()
|
||||
});
|
||||
|
||||
try {
|
||||
return SettingsSchema.parse(settings);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Warning: Invalid Codex CLI settings in config: ${error.message}. Falling back to default.`
|
||||
)
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// --- Claude Code Settings Getters ---
|
||||
|
||||
function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
|
||||
@@ -374,6 +430,23 @@ function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
|
||||
return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
|
||||
}
|
||||
|
||||
// --- Codex CLI Settings Getters ---
|
||||
|
||||
function getCodexCliSettings(explicitRoot = null, forceReload = false) {
|
||||
const config = getConfig(explicitRoot, forceReload);
|
||||
return { ...DEFAULTS.codexCli, ...(config?.codexCli || {}) };
|
||||
}
|
||||
|
||||
function getCodexCliSettingsForCommand(
|
||||
commandName,
|
||||
explicitRoot = null,
|
||||
forceReload = false
|
||||
) {
|
||||
const settings = getCodexCliSettings(explicitRoot, forceReload);
|
||||
const commandSpecific = settings?.commandSpecific || {};
|
||||
return { ...settings, ...commandSpecific[commandName] };
|
||||
}
|
||||
|
||||
function getClaudeCodeSettingsForCommand(
|
||||
commandName,
|
||||
explicitRoot = null,
|
||||
@@ -491,7 +564,8 @@ function hasCodebaseAnalysis(
|
||||
return (
|
||||
currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
|
||||
currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI ||
|
||||
currentProvider === CUSTOM_PROVIDERS.GROK_CLI
|
||||
currentProvider === CUSTOM_PROVIDERS.GROK_CLI ||
|
||||
currentProvider === CUSTOM_PROVIDERS.CODEX_CLI
|
||||
);
|
||||
}
|
||||
|
||||
@@ -721,7 +795,8 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
||||
CUSTOM_PROVIDERS.BEDROCK,
|
||||
CUSTOM_PROVIDERS.MCP,
|
||||
CUSTOM_PROVIDERS.GEMINI_CLI,
|
||||
CUSTOM_PROVIDERS.GROK_CLI
|
||||
CUSTOM_PROVIDERS.GROK_CLI,
|
||||
CUSTOM_PROVIDERS.CODEX_CLI
|
||||
];
|
||||
|
||||
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
|
||||
@@ -733,6 +808,11 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
||||
return true; // No API key needed
|
||||
}
|
||||
|
||||
// Codex CLI supports OAuth via codex login; API key optional
|
||||
if (providerName?.toLowerCase() === 'codex-cli') {
|
||||
return true; // Treat as OK even without key
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
openai: 'OPENAI_API_KEY',
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
@@ -836,6 +916,8 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
|
||||
return true; // No key needed
|
||||
case 'claude-code':
|
||||
return true; // No key needed
|
||||
case 'codex-cli':
|
||||
return true; // OAuth/subscription via Codex CLI
|
||||
case 'mistral':
|
||||
apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
|
||||
placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
|
||||
@@ -1028,7 +1110,8 @@ export const providersWithoutApiKeys = [
|
||||
CUSTOM_PROVIDERS.BEDROCK,
|
||||
CUSTOM_PROVIDERS.GEMINI_CLI,
|
||||
CUSTOM_PROVIDERS.GROK_CLI,
|
||||
CUSTOM_PROVIDERS.MCP
|
||||
CUSTOM_PROVIDERS.MCP,
|
||||
CUSTOM_PROVIDERS.CODEX_CLI
|
||||
];
|
||||
|
||||
export {
|
||||
@@ -1040,6 +1123,9 @@ export {
|
||||
// Claude Code settings
|
||||
getClaudeCodeSettings,
|
||||
getClaudeCodeSettingsForCommand,
|
||||
// Codex CLI settings
|
||||
getCodexCliSettings,
|
||||
getCodexCliSettingsForCommand,
|
||||
// Grok CLI settings
|
||||
getGrokCliSettings,
|
||||
getGrokCliSettingsForCommand,
|
||||
@@ -1047,6 +1133,7 @@ export {
|
||||
validateProvider,
|
||||
validateProviderModelCombination,
|
||||
validateClaudeCodeSettings,
|
||||
validateCodexCliSettings,
|
||||
VALIDATED_PROVIDERS,
|
||||
CUSTOM_PROVIDERS,
|
||||
ALL_PROVIDERS,
|
||||
|
||||
@@ -69,6 +69,30 @@
|
||||
"supported": true
|
||||
}
|
||||
],
|
||||
"codex-cli": [
|
||||
{
|
||||
"id": "gpt-5",
|
||||
"swe_score": 0.749,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 128000,
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-5-codex",
|
||||
"swe_score": 0.749,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 128000,
|
||||
"supported": true
|
||||
}
|
||||
],
|
||||
"mcp": [
|
||||
{
|
||||
"id": "mcp-sampling",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import path from 'path';
|
||||
|
||||
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
|
||||
import { isTaskDependentOn } from '../task-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* Add a subtask to a parent task
|
||||
@@ -142,11 +139,7 @@ async function addSubtask(
|
||||
// Write the updated tasks back to the file with proper context
|
||||
writeJSON(tasksPath, data, projectRoot, tag);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return newSubtask;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod';
|
||||
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
|
||||
|
||||
import {
|
||||
@@ -29,6 +28,7 @@ import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import ContextGatherer from '../utils/contextGatherer.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import {
|
||||
TASK_PRIORITY_OPTIONS,
|
||||
DEFAULT_TASK_PRIORITY,
|
||||
@@ -36,26 +36,6 @@ import {
|
||||
normalizeTaskPriority
|
||||
} from '../../../src/constants/task-priority.js';
|
||||
|
||||
// Define Zod schema for the expected AI output object
|
||||
const AiTaskDataSchema = z.object({
|
||||
title: z.string().describe('Clear, concise title for the task'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('A one or two sentence description of the task'),
|
||||
details: z
|
||||
.string()
|
||||
.describe('In-depth implementation details, considerations, and guidance'),
|
||||
testStrategy: z
|
||||
.string()
|
||||
.describe('Detailed approach for verifying task completion'),
|
||||
dependencies: z
|
||||
.array(z.number())
|
||||
.nullable()
|
||||
.describe(
|
||||
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||
)
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all tasks from all tags
|
||||
* @param {Object} rawData - The raw tagged data object
|
||||
@@ -451,7 +431,7 @@ async function addTask(
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
schema: AiTaskDataSchema,
|
||||
schema: COMMAND_SCHEMAS['add-task'],
|
||||
objectName: 'newTaskData',
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
displayAiUsageSummary
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
|
||||
import {
|
||||
getDebugFlag,
|
||||
@@ -29,46 +30,6 @@ 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,
|
||||
gatheredContext = ''
|
||||
) {
|
||||
const tasksString = JSON.stringify(tasksData.tasks, null, 2);
|
||||
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}`;
|
||||
|
||||
if (gatheredContext) {
|
||||
prompt += `\n\n# Project Context\n\n${gatheredContext}`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Respond ONLY with a valid JSON array matching the schema:
|
||||
[
|
||||
{
|
||||
"taskId": <number>,
|
||||
"taskTitle": "<string>",
|
||||
"complexityScore": <number 1-10>,
|
||||
"recommendedSubtasks": <number>,
|
||||
"expansionPrompt": "<string>",
|
||||
"reasoning": "<string>"
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes task complexity and generates expansion recommendations
|
||||
* @param {Object} options Command options
|
||||
@@ -446,12 +407,14 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
try {
|
||||
const role = useResearch ? 'research' : 'main';
|
||||
|
||||
aiServiceResponse = await generateTextService({
|
||||
aiServiceResponse = await generateObjectService({
|
||||
prompt,
|
||||
systemPrompt,
|
||||
role,
|
||||
session,
|
||||
projectRoot,
|
||||
schema: COMMAND_SCHEMAS['analyze-complexity'],
|
||||
objectName: 'complexityAnalysis',
|
||||
commandName: 'analyze-complexity',
|
||||
outputType: mcpLog ? 'mcp' : 'cli'
|
||||
});
|
||||
@@ -463,63 +426,15 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
if (outputFormat === 'text') {
|
||||
readline.clearLine(process.stdout, 0);
|
||||
readline.cursorTo(process.stdout, 0);
|
||||
console.log(
|
||||
chalk.green('AI service call complete. Parsing response...')
|
||||
);
|
||||
console.log(chalk.green('AI service call complete.'));
|
||||
}
|
||||
|
||||
reportLog('Parsing complexity analysis from text response...', 'info');
|
||||
try {
|
||||
let cleanedResponse = aiServiceResponse.mainResult;
|
||||
cleanedResponse = cleanedResponse.trim();
|
||||
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```(?:json)?\s*([\s\S]*?)\s*```/
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
} else {
|
||||
const firstBracket = cleanedResponse.indexOf('[');
|
||||
const lastBracket = cleanedResponse.lastIndexOf(']');
|
||||
if (firstBracket !== -1 && lastBracket > firstBracket) {
|
||||
cleanedResponse = cleanedResponse.substring(
|
||||
firstBracket,
|
||||
lastBracket + 1
|
||||
);
|
||||
} else {
|
||||
reportLog(
|
||||
'Warning: Response does not appear to be a JSON array.',
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormat === 'text' && getDebugFlag(session)) {
|
||||
console.log(chalk.gray('Attempting to parse cleaned JSON...'));
|
||||
console.log(chalk.gray('Cleaned response (first 100 chars):'));
|
||||
console.log(chalk.gray(cleanedResponse.substring(0, 100)));
|
||||
console.log(chalk.gray('Last 100 chars:'));
|
||||
console.log(
|
||||
chalk.gray(cleanedResponse.substring(cleanedResponse.length - 100))
|
||||
);
|
||||
}
|
||||
|
||||
complexityAnalysis = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
reportLog(
|
||||
`Error parsing complexity analysis JSON: ${parseError.message}`,
|
||||
'error'
|
||||
);
|
||||
if (outputFormat === 'text') {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error parsing complexity analysis JSON: ${parseError.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
throw parseError;
|
||||
}
|
||||
// With generateObject, we get structured data directly
|
||||
complexityAnalysis = aiServiceResponse.mainResult.complexityAnalysis;
|
||||
reportLog(
|
||||
`Received ${complexityAnalysis.length} complexity analyses from AI.`,
|
||||
'info'
|
||||
);
|
||||
|
||||
const taskIds = tasksData.tasks.map((t) => t.id);
|
||||
const analysisTaskIds = complexityAnalysis.map((a) => a.taskId);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
getTagAwareFilePath,
|
||||
isSilentMode,
|
||||
log,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
isSilentMode,
|
||||
getTagAwareFilePath
|
||||
writeJSON
|
||||
} from '../utils.js';
|
||||
|
||||
import {
|
||||
displayAiUsageSummary,
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator,
|
||||
displayAiUsageSummary
|
||||
stopLoadingIndicator
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
|
||||
import {
|
||||
getDefaultSubtasks,
|
||||
@@ -24,259 +24,12 @@ import {
|
||||
hasCodebaseAnalysis
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
|
||||
import { findProjectRoot, flattenTasksWithSubtasks } from '../utils.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.strictObject({
|
||||
id: z.int().positive().describe('Sequential subtask ID starting from 1'),
|
||||
title: z.string().min(5).describe('Clear, specific title for the subtask'),
|
||||
description: z
|
||||
.string()
|
||||
.min(10)
|
||||
.describe('Detailed description of the subtask'),
|
||||
dependencies: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
'Array of subtask dependencies within the same parent task. Use format ["parentTaskId.1", "parentTaskId.2"]. Subtasks can only depend on siblings, not external tasks.'
|
||||
),
|
||||
details: z.string().min(20).describe('Implementation details and guidance'),
|
||||
status: z
|
||||
.string()
|
||||
.describe(
|
||||
'The current status of the subtask (should be pending initially)'
|
||||
),
|
||||
testStrategy: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('Approach for testing this subtask')
|
||||
.prefault('')
|
||||
});
|
||||
const subtaskArraySchema = z.array(subtaskSchema);
|
||||
const subtaskWrapperSchema = z.object({
|
||||
subtasks: subtaskArraySchema.describe('The array of generated subtasks.')
|
||||
});
|
||||
// --- End Zod Schemas ---
|
||||
|
||||
/**
|
||||
* Parse subtasks from AI's text response. Includes basic cleanup.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} startId - Starting subtask ID expected.
|
||||
* @param {number} expectedCount - Expected number of subtasks.
|
||||
* @param {number} parentTaskId - Parent task ID for context.
|
||||
* @param {Object} logger - Logging object (mcpLog or console log).
|
||||
* @returns {Array} Parsed and potentially corrected subtasks array.
|
||||
* @throws {Error} If parsing fails or JSON is invalid/malformed.
|
||||
*/
|
||||
function parseSubtasksFromText(
|
||||
text,
|
||||
startId,
|
||||
expectedCount,
|
||||
parentTaskId,
|
||||
logger
|
||||
) {
|
||||
if (typeof text !== 'string') {
|
||||
logger.error(
|
||||
`AI response text is not a string. Received type: ${typeof text}, Value: ${text}`
|
||||
);
|
||||
throw new Error('AI response text is not a string.');
|
||||
}
|
||||
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('AI response text is empty after trimming.');
|
||||
}
|
||||
|
||||
const originalTrimmedResponse = text.trim(); // Store the original trimmed response
|
||||
let jsonToParse = originalTrimmedResponse; // Initialize jsonToParse with it
|
||||
|
||||
logger.debug(
|
||||
`Original AI Response for parsing (full length: ${jsonToParse.length}): ${jsonToParse.substring(0, 1000)}...`
|
||||
);
|
||||
|
||||
// --- Pre-emptive cleanup for known AI JSON issues ---
|
||||
// Fix for "dependencies": , or "dependencies":,
|
||||
if (jsonToParse.includes('"dependencies":')) {
|
||||
const malformedPattern = /"dependencies":\s*,/g;
|
||||
if (malformedPattern.test(jsonToParse)) {
|
||||
logger.warn('Attempting to fix malformed "dependencies": , issue.');
|
||||
jsonToParse = jsonToParse.replace(
|
||||
malformedPattern,
|
||||
'"dependencies": [],'
|
||||
);
|
||||
logger.debug(
|
||||
`JSON after fixing "dependencies": ${jsonToParse.substring(0, 500)}...`
|
||||
);
|
||||
}
|
||||
}
|
||||
// --- End pre-emptive cleanup ---
|
||||
|
||||
let parsedObject;
|
||||
let primaryParseAttemptFailed = false;
|
||||
|
||||
// --- Attempt 1: Simple Parse (with optional Markdown cleanup) ---
|
||||
logger.debug('Attempting simple parse...');
|
||||
try {
|
||||
// Check for markdown code block
|
||||
const codeBlockMatch = jsonToParse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
||||
let contentToParseDirectly = jsonToParse;
|
||||
if (codeBlockMatch && codeBlockMatch[1]) {
|
||||
contentToParseDirectly = codeBlockMatch[1].trim();
|
||||
logger.debug('Simple parse: Extracted content from markdown code block.');
|
||||
} else {
|
||||
logger.debug(
|
||||
'Simple parse: No markdown code block found, using trimmed original.'
|
||||
);
|
||||
}
|
||||
|
||||
parsedObject = JSON.parse(contentToParseDirectly);
|
||||
logger.debug('Simple parse successful!');
|
||||
|
||||
// Quick check if it looks like our target object
|
||||
if (
|
||||
!parsedObject ||
|
||||
typeof parsedObject !== 'object' ||
|
||||
!Array.isArray(parsedObject.subtasks)
|
||||
) {
|
||||
logger.warn(
|
||||
'Simple parse succeeded, but result is not the expected {"subtasks": []} structure. Will proceed to advanced extraction.'
|
||||
);
|
||||
primaryParseAttemptFailed = true;
|
||||
parsedObject = null; // Reset parsedObject so we enter the advanced logic
|
||||
}
|
||||
// If it IS the correct structure, we'll skip advanced extraction.
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Simple parse failed: ${e.message}. Proceeding to advanced extraction logic.`
|
||||
);
|
||||
primaryParseAttemptFailed = true;
|
||||
// jsonToParse is already originalTrimmedResponse if simple parse failed before modifying it for markdown
|
||||
}
|
||||
|
||||
// --- Attempt 2: Advanced Extraction (if simple parse failed or produced wrong structure) ---
|
||||
if (primaryParseAttemptFailed || !parsedObject) {
|
||||
// Ensure we try advanced if simple parse gave wrong structure
|
||||
logger.debug('Attempting advanced extraction logic...');
|
||||
// Reset jsonToParse to the original full trimmed response for advanced logic
|
||||
jsonToParse = originalTrimmedResponse;
|
||||
|
||||
// (Insert the more complex extraction logic here - the one we worked on with:
|
||||
// - targetPattern = '{"subtasks":';
|
||||
// - careful brace counting for that targetPattern
|
||||
// - fallbacks to last '{' and '}' if targetPattern logic fails)
|
||||
// This was the logic from my previous message. Let's assume it's here.
|
||||
// This block should ultimately set `jsonToParse` to the best candidate string.
|
||||
|
||||
// Example snippet of that advanced logic's start:
|
||||
const targetPattern = '{"subtasks":';
|
||||
const patternStartIndex = jsonToParse.indexOf(targetPattern);
|
||||
|
||||
if (patternStartIndex !== -1) {
|
||||
const openBraces = 0;
|
||||
const firstBraceFound = false;
|
||||
const extractedJsonBlock = '';
|
||||
// ... (loop for brace counting as before) ...
|
||||
// ... (if successful, jsonToParse = extractedJsonBlock) ...
|
||||
// ... (if that fails, fallbacks as before) ...
|
||||
} else {
|
||||
// ... (fallback to last '{' and '}' if targetPattern not found) ...
|
||||
}
|
||||
// End of advanced logic excerpt
|
||||
|
||||
logger.debug(
|
||||
`Advanced extraction: JSON string that will be parsed: ${jsonToParse.substring(0, 500)}...`
|
||||
);
|
||||
try {
|
||||
parsedObject = JSON.parse(jsonToParse);
|
||||
logger.debug('Advanced extraction parse successful!');
|
||||
} catch (parseError) {
|
||||
logger.error(
|
||||
`Advanced extraction: Failed to parse JSON object: ${parseError.message}`
|
||||
);
|
||||
logger.error(
|
||||
`Advanced extraction: Problematic JSON string for parse (first 500 chars): ${jsonToParse.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
// Re-throw a more specific error if advanced also fails
|
||||
`Failed to parse JSON response object after both simple and advanced attempts: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Validation (applies to successfully parsedObject from either attempt) ---
|
||||
if (
|
||||
!parsedObject ||
|
||||
typeof parsedObject !== 'object' ||
|
||||
!Array.isArray(parsedObject.subtasks)
|
||||
) {
|
||||
logger.error(
|
||||
`Final parsed content is not an object or missing 'subtasks' array. Content: ${JSON.stringify(parsedObject).substring(0, 200)}`
|
||||
);
|
||||
throw new Error(
|
||||
'Parsed AI response is not a valid object containing a "subtasks" array after all attempts.'
|
||||
);
|
||||
}
|
||||
const parsedSubtasks = parsedObject.subtasks;
|
||||
|
||||
if (expectedCount && parsedSubtasks.length !== expectedCount) {
|
||||
logger.warn(
|
||||
`Expected ${expectedCount} subtasks, but parsed ${parsedSubtasks.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
let currentId = startId;
|
||||
const validatedSubtasks = [];
|
||||
const validationErrors = [];
|
||||
|
||||
for (const rawSubtask of parsedSubtasks) {
|
||||
const correctedSubtask = {
|
||||
...rawSubtask,
|
||||
id: currentId,
|
||||
dependencies: Array.isArray(rawSubtask.dependencies)
|
||||
? rawSubtask.dependencies.filter(
|
||||
(dep) =>
|
||||
typeof dep === 'string' && dep.startsWith(`${parentTaskId}.`)
|
||||
)
|
||||
: [],
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
const result = subtaskSchema.safeParse(correctedSubtask);
|
||||
|
||||
if (result.success) {
|
||||
validatedSubtasks.push(result.data);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Subtask validation failed for raw data: ${JSON.stringify(rawSubtask).substring(0, 100)}...`
|
||||
);
|
||||
result.error.errors.forEach((err) => {
|
||||
const errorMessage = ` - Field '${err.path.join('.')}': ${err.message}`;
|
||||
logger.warn(errorMessage);
|
||||
validationErrors.push(`Subtask ${currentId}: ${errorMessage}`);
|
||||
});
|
||||
}
|
||||
currentId++;
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
logger.error(
|
||||
`Found ${validationErrors.length} validation errors in the generated subtasks.`
|
||||
);
|
||||
logger.warn('Proceeding with only the successfully validated subtasks.');
|
||||
}
|
||||
|
||||
if (validatedSubtasks.length === 0 && parsedSubtasks.length > 0) {
|
||||
throw new Error(
|
||||
'AI response contained potential subtasks, but none passed validation.'
|
||||
);
|
||||
}
|
||||
return validatedSubtasks.slice(0, expectedCount || validatedSubtasks.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a task into subtasks using the unified AI service (generateTextService).
|
||||
* Expand a task into subtasks using the unified AI service (generateObjectService).
|
||||
* Appends new subtasks by default. Replaces existing subtasks if force=true.
|
||||
* Integrates complexity report to determine subtask count and prompt if available,
|
||||
* unless numSubtasks is explicitly provided.
|
||||
@@ -444,6 +197,10 @@ async function expandTask(
|
||||
}
|
||||
|
||||
// Determine prompt content AND system prompt
|
||||
// Calculate the next subtask ID to match current behavior:
|
||||
// - Start from the number of existing subtasks + 1
|
||||
// - This creates sequential IDs: 1, 2, 3, 4...
|
||||
// - Display format shows as parentTaskId.subtaskId (e.g., "1.1", "1.2", "2.1")
|
||||
const nextSubtaskId = (task.subtasks?.length || 0) + 1;
|
||||
|
||||
// Load prompts using PromptManager
|
||||
@@ -504,7 +261,6 @@ async function expandTask(
|
||||
hasCodebaseAnalysis: hasCodebaseAnalysisCapability,
|
||||
projectRoot: projectRoot || ''
|
||||
};
|
||||
|
||||
let variantKey = 'default';
|
||||
if (expansionPromptText) {
|
||||
variantKey = 'complexity-report';
|
||||
@@ -534,7 +290,7 @@ async function expandTask(
|
||||
);
|
||||
// --- End Complexity Report / Prompt Logic ---
|
||||
|
||||
// --- AI Subtask Generation using generateTextService ---
|
||||
// --- AI Subtask Generation using generateObjectService ---
|
||||
let generatedSubtasks = [];
|
||||
let loadingIndicator = null;
|
||||
if (outputFormat === 'text') {
|
||||
@@ -543,48 +299,36 @@ async function expandTask(
|
||||
);
|
||||
}
|
||||
|
||||
let responseText = '';
|
||||
let aiServiceResponse = null;
|
||||
|
||||
try {
|
||||
const role = useResearch ? 'research' : 'main';
|
||||
|
||||
// Call generateTextService with the determined prompts and telemetry params
|
||||
aiServiceResponse = await generateTextService({
|
||||
// Call generateObjectService with the determined prompts and telemetry params
|
||||
aiServiceResponse = await generateObjectService({
|
||||
prompt: promptContent,
|
||||
systemPrompt: systemPrompt,
|
||||
role,
|
||||
session,
|
||||
projectRoot,
|
||||
schema: COMMAND_SCHEMAS['expand-task'],
|
||||
objectName: 'subtasks',
|
||||
commandName: 'expand-task',
|
||||
outputType: outputFormat
|
||||
});
|
||||
responseText = aiServiceResponse.mainResult;
|
||||
|
||||
// Parse Subtasks
|
||||
generatedSubtasks = parseSubtasksFromText(
|
||||
responseText,
|
||||
nextSubtaskId,
|
||||
finalSubtaskCount,
|
||||
task.id,
|
||||
logger
|
||||
);
|
||||
logger.info(
|
||||
`Successfully parsed ${generatedSubtasks.length} subtasks from AI response.`
|
||||
);
|
||||
// With generateObject, we expect structured data – verify it before use
|
||||
const mainResult = aiServiceResponse?.mainResult;
|
||||
if (!mainResult || !Array.isArray(mainResult.subtasks)) {
|
||||
throw new Error('AI response did not include a valid subtasks array.');
|
||||
}
|
||||
generatedSubtasks = mainResult.subtasks;
|
||||
logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
|
||||
} catch (error) {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
logger.error(
|
||||
`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
|
||||
'error'
|
||||
);
|
||||
// Log raw response in debug mode if parsing failed
|
||||
if (
|
||||
error.message.includes('Failed to parse valid subtasks') &&
|
||||
getDebugFlag(session)
|
||||
) {
|
||||
logger.error(`Raw AI Response that failed parsing:\n${responseText}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
@@ -539,6 +539,22 @@ async function setModel(role, modelId, options = {}) {
|
||||
warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
|
||||
report('warn', warningMessage);
|
||||
}
|
||||
} else if (providerHint === CUSTOM_PROVIDERS.CODEX_CLI) {
|
||||
// Codex CLI provider - enforce supported model list
|
||||
determinedProvider = CUSTOM_PROVIDERS.CODEX_CLI;
|
||||
const codexCliModels = availableModels.filter(
|
||||
(m) => m.provider === 'codex-cli'
|
||||
);
|
||||
const codexCliModelData = codexCliModels.find(
|
||||
(m) => m.id === modelId
|
||||
);
|
||||
if (codexCliModelData) {
|
||||
modelData = codexCliModelData;
|
||||
report('info', `Setting Codex CLI model '${modelId}'.`);
|
||||
} else {
|
||||
warningMessage = `Warning: Codex CLI model '${modelId}' not found in supported models. Setting without validation.`;
|
||||
report('warn', warningMessage);
|
||||
}
|
||||
} else {
|
||||
// Invalid provider hint - should not happen with our constants
|
||||
throw new Error(`Invalid provider hint received: ${providerHint}`);
|
||||
@@ -559,7 +575,7 @@ async function setModel(role, modelId, options = {}) {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'MODEL_NOT_FOUND_NO_HINT',
|
||||
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
|
||||
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, --vertex, --gemini-cli, or --codex-cli.`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
setTasksForTag,
|
||||
traverseDependencies
|
||||
} from '../utils.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import {
|
||||
findCrossTagDependencies,
|
||||
getDependentTaskIds,
|
||||
@@ -142,13 +141,7 @@ async function moveTask(
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Generate files once at the end if requested
|
||||
if (generateFiles) {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
tag: tag,
|
||||
projectRoot: projectRoot
|
||||
});
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return {
|
||||
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
||||
@@ -209,12 +202,7 @@ async function moveTask(
|
||||
// The writeJSON function will filter out _rawTaggedData automatically
|
||||
writeJSON(tasksPath, rawData, options.projectRoot, tag);
|
||||
|
||||
if (generateFiles) {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||
tag: tag,
|
||||
projectRoot: projectRoot
|
||||
});
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import path from 'path';
|
||||
import { log, readJSON, writeJSON } from '../utils.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
|
||||
/**
|
||||
* Remove a subtask from its parent task
|
||||
@@ -108,11 +106,7 @@ async function removeSubtask(
|
||||
// Write the updated tasks back to the file with proper context
|
||||
writeJSON(tasksPath, data, projectRoot, tag);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||
}
|
||||
// Note: Task file generation is no longer supported and has been removed
|
||||
|
||||
return convertedTask;
|
||||
} catch (error) {
|
||||
|
||||
@@ -619,9 +619,29 @@ async function tags(
|
||||
headers.push(chalk.cyan.bold('Description'));
|
||||
}
|
||||
|
||||
// Calculate dynamic column widths based on terminal width
|
||||
const terminalWidth = Math.max(process.stdout.columns || 120, 80);
|
||||
const usableWidth = Math.floor(terminalWidth * 0.95);
|
||||
|
||||
let colWidths;
|
||||
if (showMetadata) {
|
||||
// With metadata: Tag Name, Tasks, Completed, Created, Description
|
||||
const widths = [0.25, 0.1, 0.12, 0.15, 0.38];
|
||||
colWidths = widths.map((w, i) =>
|
||||
Math.max(Math.floor(usableWidth * w), i === 0 ? 15 : 8)
|
||||
);
|
||||
} else {
|
||||
// Without metadata: Tag Name, Tasks, Completed
|
||||
const widths = [0.7, 0.15, 0.15];
|
||||
colWidths = widths.map((w, i) =>
|
||||
Math.max(Math.floor(usableWidth * w), i === 0 ? 20 : 10)
|
||||
);
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: headers,
|
||||
colWidths: showMetadata ? [20, 10, 12, 15, 50] : [25, 10, 12]
|
||||
colWidths: colWidths,
|
||||
wordWrap: true
|
||||
});
|
||||
|
||||
// Add rows
|
||||
|
||||
@@ -3,7 +3,6 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod'; // Keep Zod for post-parse validation
|
||||
|
||||
import {
|
||||
log as consoleLog,
|
||||
@@ -22,7 +21,11 @@ import {
|
||||
displayAiUsageSummary
|
||||
} from '../ui.js';
|
||||
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import {
|
||||
generateTextService,
|
||||
generateObjectService
|
||||
} from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
isApiKeySet,
|
||||
@@ -32,229 +35,6 @@ import { getPromptManager } from '../prompt-manager.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
|
||||
.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(), // Title should be preserved, but check it exists
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.union([z.number().int(), z.string()])),
|
||||
priority: z.string().nullable().prefault('medium'),
|
||||
details: z.string().nullable().prefault(''),
|
||||
testStrategy: z.string().nullable().prefault(''),
|
||||
subtasks: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe('Sequential subtask ID starting from 1'),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.number().int()).nullable().prefault([]),
|
||||
details: z.string().nullable().prefault(''),
|
||||
testStrategy: z.string().nullable().prefault('')
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
.prefault([])
|
||||
})
|
||||
.strip(); // Enforce the canonical task shape and drop unknown fields
|
||||
|
||||
/**
|
||||
* Parses a single updated task object from AI's text response.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} expectedTaskId - The ID of the task expected.
|
||||
* @param {Function | Object} logFn - Logging function or MCP logger.
|
||||
* @param {boolean} isMCP - Flag indicating MCP context.
|
||||
* @returns {Object} Parsed and validated task object.
|
||||
* @throws {Error} If parsing or validation fails.
|
||||
*/
|
||||
function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
|
||||
// Report helper consistent with the established pattern
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logFn[level] === 'function') logFn[level](...args);
|
||||
else logFn.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
logFn(level, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
report(
|
||||
'info',
|
||||
'Attempting to parse updated task object from text response...'
|
||||
);
|
||||
if (!text || text.trim() === '')
|
||||
throw new Error('AI response text is empty.');
|
||||
|
||||
let cleanedResponse = text.trim();
|
||||
const originalResponseForDebug = cleanedResponse;
|
||||
let parseMethodUsed = 'raw'; // Keep track of which method worked
|
||||
|
||||
// --- NEW Step 1: Try extracting between {} first ---
|
||||
const firstBraceIndex = cleanedResponse.indexOf('{');
|
||||
const lastBraceIndex = cleanedResponse.lastIndexOf('}');
|
||||
let potentialJsonFromBraces = null;
|
||||
|
||||
if (firstBraceIndex !== -1 && lastBraceIndex > firstBraceIndex) {
|
||||
potentialJsonFromBraces = cleanedResponse.substring(
|
||||
firstBraceIndex,
|
||||
lastBraceIndex + 1
|
||||
);
|
||||
if (potentialJsonFromBraces.length <= 2) {
|
||||
potentialJsonFromBraces = null; // Ignore empty braces {}
|
||||
}
|
||||
}
|
||||
|
||||
// If {} extraction yielded something, try parsing it immediately
|
||||
if (potentialJsonFromBraces) {
|
||||
try {
|
||||
const testParse = JSON.parse(potentialJsonFromBraces);
|
||||
// It worked! Use this as the primary cleaned response.
|
||||
cleanedResponse = potentialJsonFromBraces;
|
||||
parseMethodUsed = 'braces';
|
||||
} catch (e) {
|
||||
report(
|
||||
'info',
|
||||
'Content between {} looked promising but failed initial parse. Proceeding to other methods.'
|
||||
);
|
||||
// Reset cleanedResponse to original if brace parsing failed
|
||||
cleanedResponse = originalResponseForDebug;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 2: If brace parsing didn't work or wasn't applicable, try code block extraction ---
|
||||
if (parseMethodUsed === 'raw') {
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```(?:json|javascript)?\s*([\s\S]*?)\s*```/i
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
parseMethodUsed = 'codeblock';
|
||||
report('info', 'Extracted JSON content from Markdown code block.');
|
||||
} else {
|
||||
// --- Step 3: If code block failed, try stripping prefixes ---
|
||||
const commonPrefixes = [
|
||||
'json\n',
|
||||
'javascript\n'
|
||||
// ... other prefixes ...
|
||||
];
|
||||
let prefixFound = false;
|
||||
for (const prefix of commonPrefixes) {
|
||||
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
|
||||
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
|
||||
parseMethodUsed = 'prefix';
|
||||
report('info', `Stripped prefix: "${prefix.trim()}"`);
|
||||
prefixFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefixFound) {
|
||||
report(
|
||||
'warn',
|
||||
'Response does not appear to contain {}, code block, or known prefix. Attempting raw parse.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 4: Attempt final parse ---
|
||||
let parsedTask;
|
||||
try {
|
||||
parsedTask = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
report('error', `Failed to parse JSON object: ${parseError.message}`);
|
||||
report(
|
||||
'error',
|
||||
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to parse JSON response object: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsedTask || typeof parsedTask !== 'object') {
|
||||
report(
|
||||
'error',
|
||||
`Parsed content is not an object. Type: ${typeof parsedTask}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Parsed content sample: ${JSON.stringify(parsedTask).substring(0, 200)}`
|
||||
);
|
||||
throw new Error('Parsed AI response is not a valid JSON object.');
|
||||
}
|
||||
|
||||
// Preprocess the task to ensure subtasks have proper structure
|
||||
const preprocessedTask = {
|
||||
...parsedTask,
|
||||
status: parsedTask.status || 'pending',
|
||||
dependencies: Array.isArray(parsedTask.dependencies)
|
||||
? parsedTask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof parsedTask.details === 'string'
|
||||
? parsedTask.details
|
||||
: String(parsedTask.details || ''),
|
||||
testStrategy:
|
||||
typeof parsedTask.testStrategy === 'string'
|
||||
? parsedTask.testStrategy
|
||||
: String(parsedTask.testStrategy || ''),
|
||||
// Ensure subtasks is an array and each subtask has required fields
|
||||
subtasks: Array.isArray(parsedTask.subtasks)
|
||||
? parsedTask.subtasks.map((subtask) => ({
|
||||
...subtask,
|
||||
title: subtask.title || '',
|
||||
description: subtask.description || '',
|
||||
status: subtask.status || 'pending',
|
||||
dependencies: Array.isArray(subtask.dependencies)
|
||||
? subtask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof subtask.details === 'string'
|
||||
? subtask.details
|
||||
: String(subtask.details || ''),
|
||||
testStrategy:
|
||||
typeof subtask.testStrategy === 'string'
|
||||
? subtask.testStrategy
|
||||
: String(subtask.testStrategy || '')
|
||||
}))
|
||||
: []
|
||||
};
|
||||
|
||||
// Validate the parsed task object using Zod
|
||||
const validationResult = updatedTaskSchema.safeParse(preprocessedTask);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task object failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
report('error', ` - Field '${err.path.join('.')}': ${err.message}`);
|
||||
});
|
||||
throw new Error(
|
||||
`AI response failed task structure validation: ${validationResult.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Final check: ensure ID matches expected ID (AI might hallucinate)
|
||||
if (validationResult.data.id !== expectedTaskId) {
|
||||
report(
|
||||
'warn',
|
||||
`AI returned task with ID ${validationResult.data.id}, but expected ${expectedTaskId}. Overwriting ID.`
|
||||
);
|
||||
validationResult.data.id = expectedTaskId; // Enforce correct ID
|
||||
}
|
||||
|
||||
report('info', 'Successfully validated updated task structure.');
|
||||
return validationResult.data; // Return the validated task data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task by ID with new information using the unified AI service.
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
@@ -522,15 +302,32 @@ async function updateTaskById(
|
||||
|
||||
try {
|
||||
const serviceRole = useResearch ? 'research' : 'main';
|
||||
aiServiceResponse = await generateTextService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
commandName: 'update-task',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
|
||||
if (appendMode) {
|
||||
// Append mode still uses generateTextService since it returns plain text
|
||||
aiServiceResponse = await generateTextService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
commandName: 'update-task',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
} else {
|
||||
// Full update mode uses generateObjectService for structured output
|
||||
aiServiceResponse = await generateObjectService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
schema: COMMAND_SCHEMAS['update-task-by-id'],
|
||||
objectName: 'task',
|
||||
commandName: 'update-task',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
}
|
||||
|
||||
if (loadingIndicator)
|
||||
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
|
||||
@@ -600,13 +397,8 @@ async function updateTaskById(
|
||||
};
|
||||
}
|
||||
|
||||
// Full update mode: Use mainResult (text) for parsing
|
||||
const updatedTask = parseUpdatedTaskFromText(
|
||||
aiServiceResponse.mainResult,
|
||||
taskId,
|
||||
logFn,
|
||||
isMCP
|
||||
);
|
||||
// Full update mode: Use structured data directly
|
||||
const updatedTask = aiServiceResponse.mainResult.task;
|
||||
|
||||
// --- Task Validation/Correction (Keep existing logic) ---
|
||||
if (!updatedTask || typeof updatedTask !== 'object')
|
||||
|
||||
@@ -2,7 +2,6 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { z } from 'zod'; // Keep Zod for post-parsing validation
|
||||
|
||||
import {
|
||||
log as consoleLog,
|
||||
@@ -22,258 +21,13 @@ import {
|
||||
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.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
|
||||
.object({
|
||||
id: z.int(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.string(),
|
||||
dependencies: z.array(z.union([z.int(), z.string()])),
|
||||
priority: z.string().nullable(),
|
||||
details: z.string().nullable(),
|
||||
testStrategy: z.string().nullable(),
|
||||
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
|
||||
})
|
||||
.strip(); // Enforce the canonical task shape and drop unknown fields
|
||||
|
||||
// Preprocessing schema that adds defaults before validation
|
||||
const preprocessTaskSchema = z.preprocess((task) => {
|
||||
// Ensure task is an object
|
||||
if (typeof task !== 'object' || task === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Return task with defaults for missing fields
|
||||
return {
|
||||
...task,
|
||||
// Add defaults for required fields if missing
|
||||
id: task.id ?? 0,
|
||||
title: task.title ?? 'Untitled Task',
|
||||
description: task.description ?? '',
|
||||
status: task.status ?? 'pending',
|
||||
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
|
||||
// Optional fields - preserve undefined/null distinction
|
||||
priority: task.hasOwnProperty('priority') ? task.priority : null,
|
||||
details: task.hasOwnProperty('details') ? task.details : null,
|
||||
testStrategy: task.hasOwnProperty('testStrategy')
|
||||
? task.testStrategy
|
||||
: null,
|
||||
subtasks: Array.isArray(task.subtasks)
|
||||
? task.subtasks
|
||||
: task.subtasks === null
|
||||
? null
|
||||
: []
|
||||
};
|
||||
}, updatedTaskSchema);
|
||||
|
||||
const updatedTaskArraySchema = z.array(updatedTaskSchema);
|
||||
const preprocessedTaskArraySchema = z.array(preprocessTaskSchema);
|
||||
|
||||
/**
|
||||
* Parses an array of task objects from AI's text response.
|
||||
* @param {string} text - Response text from AI.
|
||||
* @param {number} expectedCount - Expected number of tasks.
|
||||
* @param {Function | Object} logFn - The logging function or MCP log object.
|
||||
* @param {boolean} isMCP - Flag indicating if logFn is MCP logger.
|
||||
* @returns {Array} Parsed and validated tasks array.
|
||||
* @throws {Error} If parsing or validation fails.
|
||||
*/
|
||||
function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logFn[level] === 'function') logFn[level](...args);
|
||||
else logFn.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
// Check silent mode for consoleLog
|
||||
consoleLog(level, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
report(
|
||||
'info',
|
||||
'Attempting to parse updated tasks array from text response...'
|
||||
);
|
||||
if (!text || text.trim() === '')
|
||||
throw new Error('AI response text is empty.');
|
||||
|
||||
let cleanedResponse = text.trim();
|
||||
const originalResponseForDebug = cleanedResponse;
|
||||
let parseMethodUsed = 'raw'; // Track which method worked
|
||||
|
||||
// --- NEW Step 1: Try extracting between [] first ---
|
||||
const firstBracketIndex = cleanedResponse.indexOf('[');
|
||||
const lastBracketIndex = cleanedResponse.lastIndexOf(']');
|
||||
let potentialJsonFromArray = null;
|
||||
|
||||
if (firstBracketIndex !== -1 && lastBracketIndex > firstBracketIndex) {
|
||||
potentialJsonFromArray = cleanedResponse.substring(
|
||||
firstBracketIndex,
|
||||
lastBracketIndex + 1
|
||||
);
|
||||
// Basic check to ensure it's not just "[]" or malformed
|
||||
if (potentialJsonFromArray.length <= 2) {
|
||||
potentialJsonFromArray = null; // Ignore empty array
|
||||
}
|
||||
}
|
||||
|
||||
// If [] extraction yielded something, try parsing it immediately
|
||||
if (potentialJsonFromArray) {
|
||||
try {
|
||||
const testParse = JSON.parse(potentialJsonFromArray);
|
||||
// It worked! Use this as the primary cleaned response.
|
||||
cleanedResponse = potentialJsonFromArray;
|
||||
parseMethodUsed = 'brackets';
|
||||
} catch (e) {
|
||||
report(
|
||||
'info',
|
||||
'Content between [] looked promising but failed initial parse. Proceeding to other methods.'
|
||||
);
|
||||
// Reset cleanedResponse to original if bracket parsing failed
|
||||
cleanedResponse = originalResponseForDebug;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 2: If bracket parsing didn't work or wasn't applicable, try code block extraction ---
|
||||
if (parseMethodUsed === 'raw') {
|
||||
// Only look for ```json blocks now
|
||||
const codeBlockMatch = cleanedResponse.match(
|
||||
/```json\s*([\s\S]*?)\s*```/i // Only match ```json
|
||||
);
|
||||
if (codeBlockMatch) {
|
||||
cleanedResponse = codeBlockMatch[1].trim();
|
||||
parseMethodUsed = 'codeblock';
|
||||
report('info', 'Extracted JSON content from JSON Markdown code block.');
|
||||
} else {
|
||||
report('info', 'No JSON code block found.');
|
||||
// --- Step 3: If code block failed, try stripping prefixes ---
|
||||
const commonPrefixes = [
|
||||
'json\n',
|
||||
'javascript\n', // Keep checking common prefixes just in case
|
||||
'python\n',
|
||||
'here are the updated tasks:',
|
||||
'here is the updated json:',
|
||||
'updated tasks:',
|
||||
'updated json:',
|
||||
'response:',
|
||||
'output:'
|
||||
];
|
||||
let prefixFound = false;
|
||||
for (const prefix of commonPrefixes) {
|
||||
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
|
||||
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
|
||||
parseMethodUsed = 'prefix';
|
||||
report('info', `Stripped prefix: "${prefix.trim()}"`);
|
||||
prefixFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefixFound) {
|
||||
report(
|
||||
'warn',
|
||||
'Response does not appear to contain [], JSON code block, or known prefix. Attempting raw parse.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 4: Attempt final parse ---
|
||||
let parsedTasks;
|
||||
try {
|
||||
parsedTasks = JSON.parse(cleanedResponse);
|
||||
} catch (parseError) {
|
||||
report('error', `Failed to parse JSON array: ${parseError.message}`);
|
||||
report(
|
||||
'error',
|
||||
`Extraction method used: ${parseMethodUsed}` // Log which method failed
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to parse JSON response array: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// --- Step 5 & 6: Validate Array structure and Zod schema ---
|
||||
if (!Array.isArray(parsedTasks)) {
|
||||
report(
|
||||
'error',
|
||||
`Parsed content is not an array. Type: ${typeof parsedTasks}`
|
||||
);
|
||||
report(
|
||||
'error',
|
||||
`Parsed content sample: ${JSON.stringify(parsedTasks).substring(0, 200)}`
|
||||
);
|
||||
throw new Error('Parsed AI response is not a valid JSON array.');
|
||||
}
|
||||
|
||||
report('info', `Successfully parsed ${parsedTasks.length} potential tasks.`);
|
||||
if (expectedCount && parsedTasks.length !== expectedCount) {
|
||||
report(
|
||||
'warn',
|
||||
`Expected ${expectedCount} tasks, but parsed ${parsedTasks.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Log missing fields for debugging before preprocessing
|
||||
let hasWarnings = false;
|
||||
parsedTasks.forEach((task, index) => {
|
||||
const missingFields = [];
|
||||
if (!task.hasOwnProperty('id')) missingFields.push('id');
|
||||
if (!task.hasOwnProperty('status')) missingFields.push('status');
|
||||
if (!task.hasOwnProperty('dependencies'))
|
||||
missingFields.push('dependencies');
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
hasWarnings = true;
|
||||
report(
|
||||
'warn',
|
||||
`Task ${index} is missing fields: ${missingFields.join(', ')} - will use defaults`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasWarnings) {
|
||||
report(
|
||||
'warn',
|
||||
'Some tasks were missing required fields. Applying defaults...'
|
||||
);
|
||||
}
|
||||
|
||||
// Use the preprocessing schema to add defaults and validate
|
||||
const preprocessResult = preprocessedTaskArraySchema.safeParse(parsedTasks);
|
||||
|
||||
if (!preprocessResult.success) {
|
||||
// This should rarely happen now since preprocessing adds defaults
|
||||
report('error', 'Failed to validate task array even after preprocessing.');
|
||||
preprocessResult.error.errors.forEach((err) => {
|
||||
report('error', ` - Path '${err.path.join('.')}': ${err.message}`);
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
`AI response failed validation: ${preprocessResult.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
report('info', 'Successfully validated and transformed task structure.');
|
||||
return preprocessResult.data.slice(
|
||||
0,
|
||||
expectedCount || preprocessResult.data.length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tasks based on new context using the unified AI service.
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
@@ -458,13 +212,15 @@ async function updateTasks(
|
||||
// Determine role based on research flag
|
||||
const serviceRole = useResearch ? 'research' : 'main';
|
||||
|
||||
// Call the unified AI service
|
||||
aiServiceResponse = await generateTextService({
|
||||
// Call the unified AI service with generateObject
|
||||
aiServiceResponse = await generateObjectService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
projectRoot: projectRoot,
|
||||
systemPrompt: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
schema: COMMAND_SCHEMAS['update-tasks'],
|
||||
objectName: 'tasks',
|
||||
commandName: 'update-tasks',
|
||||
outputType: isMCP ? 'mcp' : 'cli'
|
||||
});
|
||||
@@ -472,13 +228,8 @@ async function updateTasks(
|
||||
if (loadingIndicator)
|
||||
stopLoadingIndicator(loadingIndicator, 'AI update complete.');
|
||||
|
||||
// Use the mainResult (text) for parsing
|
||||
const parsedUpdatedTasks = parseUpdatedTasksFromText(
|
||||
aiServiceResponse.mainResult,
|
||||
tasksToUpdate.length,
|
||||
logFn,
|
||||
isMCP
|
||||
);
|
||||
// With generateObject, we get structured data directly
|
||||
const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
|
||||
|
||||
// --- Update Tasks Data (Updated writeJSON call) ---
|
||||
if (!Array.isArray(parsedUpdatedTasks)) {
|
||||
|
||||
@@ -2310,7 +2310,8 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
|
||||
outputTokens,
|
||||
totalTokens,
|
||||
totalCost,
|
||||
commandName
|
||||
commandName,
|
||||
isUnknownCost
|
||||
} = telemetryData;
|
||||
|
||||
let summary = chalk.bold.blue('AI Usage Summary:') + '\n';
|
||||
@@ -2320,7 +2321,10 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
|
||||
summary += chalk.gray(
|
||||
` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n`
|
||||
);
|
||||
summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`);
|
||||
|
||||
// Show "Unknown" if pricing data is not available, otherwise show the cost
|
||||
const costDisplay = isUnknownCost ? 'Unknown' : `$${totalCost.toFixed(6)}`;
|
||||
summary += chalk.gray(` Est. Cost: ${costDisplay}`);
|
||||
|
||||
console.log(
|
||||
boxen(summary, {
|
||||
|
||||
@@ -21,6 +21,20 @@ export class BaseAIProvider {
|
||||
|
||||
// Each provider must set their name
|
||||
this.name = this.constructor.name;
|
||||
|
||||
/**
|
||||
* Whether this provider needs explicit schema in JSON mode
|
||||
* Can be overridden by subclasses
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.needsExplicitJsonSchema = false;
|
||||
|
||||
/**
|
||||
* Whether this provider supports temperature parameter
|
||||
* Can be overridden by subclasses
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.supportsTemperature = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,7 +175,9 @@ export class BaseAIProvider {
|
||||
model: client(params.modelId),
|
||||
messages: params.messages,
|
||||
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
||||
temperature: params.temperature
|
||||
...(this.supportsTemperature && params.temperature !== undefined
|
||||
? { temperature: params.temperature }
|
||||
: {})
|
||||
});
|
||||
|
||||
log(
|
||||
@@ -169,12 +185,19 @@ export class BaseAIProvider {
|
||||
`${this.name} generateText completed successfully for model: ${params.modelId}`
|
||||
);
|
||||
|
||||
const inputTokens =
|
||||
result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
|
||||
const outputTokens =
|
||||
result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
|
||||
const totalTokens =
|
||||
result.usage?.totalTokens ?? inputTokens + outputTokens;
|
||||
|
||||
return {
|
||||
text: result.text,
|
||||
usage: {
|
||||
inputTokens: result.usage?.promptTokens,
|
||||
outputTokens: result.usage?.completionTokens,
|
||||
totalTokens: result.usage?.totalTokens
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
totalTokens
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -197,7 +220,9 @@ export class BaseAIProvider {
|
||||
model: client(params.modelId),
|
||||
messages: params.messages,
|
||||
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
||||
temperature: params.temperature
|
||||
...(this.supportsTemperature && params.temperature !== undefined
|
||||
? { temperature: params.temperature }
|
||||
: {})
|
||||
});
|
||||
|
||||
log(
|
||||
@@ -235,7 +260,9 @@ export class BaseAIProvider {
|
||||
schema: zodSchema(params.schema),
|
||||
mode: params.mode || 'auto',
|
||||
maxOutputTokens: params.maxTokens,
|
||||
temperature: params.temperature
|
||||
...(this.supportsTemperature && params.temperature !== undefined
|
||||
? { temperature: params.temperature }
|
||||
: {})
|
||||
});
|
||||
|
||||
log(
|
||||
@@ -272,13 +299,18 @@ export class BaseAIProvider {
|
||||
);
|
||||
|
||||
const client = await this.getClient(params);
|
||||
|
||||
const result = await generateObject({
|
||||
model: client(params.modelId),
|
||||
messages: params.messages,
|
||||
schema: zodSchema(params.schema),
|
||||
mode: params.mode || 'auto',
|
||||
...this.prepareTokenParam(params.modelId, params.maxTokens),
|
||||
temperature: params.temperature
|
||||
schema: params.schema,
|
||||
mode: this.needsExplicitJsonSchema ? 'json' : 'auto',
|
||||
schemaName: params.objectName,
|
||||
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
|
||||
maxTokens: params.maxTokens,
|
||||
...(this.supportsTemperature && params.temperature !== undefined
|
||||
? { temperature: params.temperature }
|
||||
: {})
|
||||
});
|
||||
|
||||
log(
|
||||
@@ -286,19 +318,26 @@ export class BaseAIProvider {
|
||||
`${this.name} generateObject completed successfully for model: ${params.modelId}`
|
||||
);
|
||||
|
||||
const inputTokens =
|
||||
result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
|
||||
const outputTokens =
|
||||
result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
|
||||
const totalTokens =
|
||||
result.usage?.totalTokens ?? inputTokens + outputTokens;
|
||||
|
||||
return {
|
||||
object: result.object,
|
||||
usage: {
|
||||
inputTokens: result.usage?.promptTokens,
|
||||
outputTokens: result.usage?.completionTokens,
|
||||
totalTokens: result.usage?.totalTokens
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
totalTokens
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Check if this is a JSON parsing error that we can potentially fix
|
||||
if (
|
||||
NoObjectGeneratedError.isInstance(error) &&
|
||||
JSONParseError.isInstance(error.cause) &&
|
||||
error.cause instanceof JSONParseError &&
|
||||
error.cause.text
|
||||
) {
|
||||
log(
|
||||
|
||||
@@ -32,6 +32,10 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
||||
super();
|
||||
this.name = 'Claude Code';
|
||||
this.supportedModels = ['sonnet', 'opus'];
|
||||
// Claude Code requires explicit JSON schema mode
|
||||
this.needsExplicitJsonSchema = true;
|
||||
// Claude Code does not support temperature parameter
|
||||
this.supportsTemperature = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
106
src/ai-providers/codex-cli.js
Normal file
106
src/ai-providers/codex-cli.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* src/ai-providers/codex-cli.js
|
||||
*
|
||||
* Codex CLI provider implementation using the ai-sdk-provider-codex-cli package.
|
||||
* This provider uses the local OpenAI Codex CLI with OAuth (preferred) or
|
||||
* an optional OPENAI_CODEX_API_KEY if provided.
|
||||
*/
|
||||
|
||||
import { createCodexCli } from 'ai-sdk-provider-codex-cli';
|
||||
import { BaseAIProvider } from './base-provider.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
import { getCodexCliSettingsForCommand } from '../../scripts/modules/config-manager.js';
|
||||
|
||||
export class CodexCliProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'Codex CLI';
|
||||
// Codex CLI has native schema support, no explicit JSON schema mode required
|
||||
this.needsExplicitJsonSchema = false;
|
||||
// Codex CLI does not support temperature parameter
|
||||
this.supportsTemperature = false;
|
||||
// Restrict to supported models for OAuth subscription usage
|
||||
this.supportedModels = ['gpt-5', 'gpt-5-codex'];
|
||||
// CLI availability check cache
|
||||
this._codexCliChecked = false;
|
||||
this._codexCliAvailable = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex CLI does not require an API key when using OAuth via `codex login`.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name used when an API key is provided.
|
||||
* Even though the API key is optional for Codex CLI (OAuth-first),
|
||||
* downstream resolution expects a non-throwing implementation.
|
||||
* Uses OPENAI_CODEX_API_KEY to avoid conflicts with OpenAI provider.
|
||||
* @returns {string}
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'OPENAI_CODEX_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional CLI availability check; provide helpful guidance if missing.
|
||||
*/
|
||||
validateAuth() {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
|
||||
if (!this._codexCliChecked) {
|
||||
try {
|
||||
execSync('codex --version', { stdio: 'pipe', timeout: 1000 });
|
||||
this._codexCliAvailable = true;
|
||||
} catch (error) {
|
||||
this._codexCliAvailable = false;
|
||||
log(
|
||||
'warn',
|
||||
'Codex CLI not detected. Install with: npm i -g @openai/codex or enable fallback with allowNpx.'
|
||||
);
|
||||
} finally {
|
||||
this._codexCliChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Codex CLI client instance
|
||||
* @param {object} params
|
||||
* @param {string} [params.commandName] - Command name for settings lookup
|
||||
* @param {string} [params.apiKey] - Optional API key (injected as OPENAI_API_KEY for Codex CLI)
|
||||
* @returns {Function}
|
||||
*/
|
||||
getClient(params = {}) {
|
||||
try {
|
||||
// Merge global + command-specific settings from config
|
||||
const settings = getCodexCliSettingsForCommand(params.commandName) || {};
|
||||
|
||||
// Inject API key only if explicitly provided; OAuth is the primary path
|
||||
const defaultSettings = {
|
||||
...settings,
|
||||
...(params.apiKey
|
||||
? { env: { ...(settings.env || {}), OPENAI_API_KEY: params.apiKey } }
|
||||
: {})
|
||||
};
|
||||
|
||||
return createCodexCli({ defaultSettings });
|
||||
} catch (error) {
|
||||
const msg = String(error?.message || '');
|
||||
const code = error?.code;
|
||||
if (code === 'ENOENT' || /codex/i.test(msg)) {
|
||||
const enhancedError = new Error(
|
||||
`Codex CLI not available. Please install Codex CLI first. Original error: ${error.message}`
|
||||
);
|
||||
enhancedError.cause = error;
|
||||
this.handleError('Codex CLI initialization', enhancedError);
|
||||
} else {
|
||||
this.handleError('client initialization', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ export class GeminiCliProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'Gemini CLI';
|
||||
// Gemini CLI requires explicit JSON schema mode
|
||||
this.needsExplicitJsonSchema = true;
|
||||
// Gemini CLI does not support temperature parameter
|
||||
this.supportsTemperature = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,7 +591,7 @@ Generate ${subtaskCount} subtasks based on the original task context. Return ONL
|
||||
system: systemPrompt,
|
||||
messages: messages,
|
||||
schema: params.schema,
|
||||
mode: 'json', // Use json mode instead of auto for Gemini
|
||||
mode: this.needsExplicitJsonSchema ? 'json' : 'auto',
|
||||
maxOutputTokens: params.maxTokens,
|
||||
temperature: params.temperature
|
||||
});
|
||||
|
||||
@@ -11,6 +11,10 @@ export class GrokCliProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'Grok CLI';
|
||||
// Grok CLI requires explicit JSON schema mode
|
||||
this.needsExplicitJsonSchema = true;
|
||||
// Grok CLI does not support temperature parameter
|
||||
this.supportsTemperature = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,3 +17,4 @@ export { VertexAIProvider } from './google-vertex.js';
|
||||
export { ClaudeCodeProvider } from './claude-code.js';
|
||||
export { GeminiCliProvider } from './gemini-cli.js';
|
||||
export { GrokCliProvider } from './grok-cli.js';
|
||||
export { CodexCliProvider } from './codex-cli.js';
|
||||
|
||||
@@ -24,7 +24,8 @@ export const CUSTOM_PROVIDERS = {
|
||||
CLAUDE_CODE: 'claude-code',
|
||||
MCP: 'mcp',
|
||||
GEMINI_CLI: 'gemini-cli',
|
||||
GROK_CLI: 'grok-cli'
|
||||
GROK_CLI: 'grok-cli',
|
||||
CODEX_CLI: 'codex-cli'
|
||||
};
|
||||
|
||||
// Custom providers array (for backward compatibility and iteration)
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before analyzing task complexity:\n\n1. Use the Glob tool to explore the project structure and understand the codebase size\n2. Use the Grep tool to search for existing implementations related to each task\n3. Use the Read tool to examine key files that would be affected by these tasks\n4. Understand the current implementation state, patterns used, and technical debt\n\nBased on your codebase analysis:\n- Assess complexity based on ACTUAL code that needs to be modified/created\n- Consider existing abstractions and patterns that could simplify implementation\n- Identify tasks that require refactoring vs. greenfield development\n- Factor in dependencies between existing code and new features\n- Provide more accurate subtask recommendations based on real code structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}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.{{#if useResearch}} Consider current best practices, common implementation patterns, and industry standards in your analysis.{{/if}}\n\nTasks:\n{{{json tasks}}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nRespond ONLY with a valid JSON array matching the schema:\n[\n {\n \"taskId\": <number>,\n \"taskTitle\": \"<string>\",\n \"complexityScore\": <number 1-10>,\n \"recommendedSubtasks\": <number>,\n \"expansionPrompt\": \"<string>\",\n \"reasoning\": \"<string>\"\n },\n ...\n]\n\nDo not include any explanatory text, markdown formatting, or code block markers before or after the JSON array."
|
||||
"system": "You are an expert software architect and project manager analyzing task complexity. Your analysis should consider implementation effort, technical challenges, dependencies, and testing requirements.\n\nIMPORTANT: For each task, provide an analysis object with ALL of the following fields:\n- taskId: The ID of the task being analyzed (positive integer)\n- taskTitle: The title of the task\n- complexityScore: A score from 1-10 indicating complexity\n- recommendedSubtasks: Number of subtasks recommended (non-negative integer; 0 if no expansion needed)\n- expansionPrompt: A prompt to guide subtask generation\n- reasoning: Your reasoning for the complexity score",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before analyzing task complexity:\n\n1. Use the Glob tool to explore the project structure and understand the codebase size\n2. Use the Grep tool to search for existing implementations related to each task\n3. Use the Read tool to examine key files that would be affected by these tasks\n4. Understand the current implementation state, patterns used, and technical debt\n\nBased on your codebase analysis:\n- Assess complexity based on ACTUAL code that needs to be modified/created\n- Consider existing abstractions and patterns that could simplify implementation\n- Identify tasks that require refactoring vs. greenfield development\n- Factor in dependencies between existing code and new features\n- Provide more accurate subtask recommendations based on real code structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}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.{{#if useResearch}} Consider current best practices, common implementation patterns, and industry standards in your analysis.{{/if}}\n\nTasks:\n{{{json tasks}}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,17 +68,18 @@
|
||||
"prompts": {
|
||||
"complexity-report": {
|
||||
"condition": "expansionPrompt",
|
||||
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\nRespond ONLY with a valid JSON object containing a single key \"subtasks\" whose value is an array of the generated subtask objects.\nEach subtask object in the array must have keys: \"id\", \"title\", \"description\", \"dependencies\", \"details\", \"status\".\nEnsure the 'id' starts from {{nextSubtaskId}} and is sequential.\nFor 'dependencies', use the full subtask ID format: \"{{task.id}}.1\", \"{{task.id}}.2\", etc. Only reference subtasks within this same task.\nEnsure 'status' is 'pending'.\nDo not include any other text or explanation.",
|
||||
"user": "Break down the following task based on the analysis prompt:\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}\n\nExpansion Guidance:\n{{expansionPrompt}}{{#if additionalContext}}\n\n{{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\n\n{{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nGenerate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks with sequential IDs starting from {{nextSubtaskId}}."
|
||||
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
|
||||
|
||||
"user": "Break down the following task:\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}\n\n{{expansionPrompt}}{{#if additionalContext}}\n\n{{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\n\n{{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nGenerate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks. CRITICAL: Use sequential IDs starting from {{nextSubtaskId}} (first={{nextSubtaskId}}, second={{nextSubtaskId}}+1, etc.)."
|
||||
},
|
||||
"research": {
|
||||
"condition": "useResearch === true && !expansionPrompt",
|
||||
"system": "You are an AI assistant that responds ONLY with valid JSON objects as requested. The object should contain a 'subtasks' array.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks using your research capabilities. Assign sequential IDs starting from {{nextSubtaskId}}.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: Respond ONLY with a valid JSON object containing a single key \"subtasks\". The value must be an array of the generated subtasks, strictly matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": <number>, // Sequential ID starting from {{nextSubtaskId}}\n \"title\": \"<string>\",\n \"description\": \"<string>\",\n \"dependencies\": [\"<string>\"], // Use full subtask IDs like [\"{{task.id}}.1\", \"{{task.id}}.2\"]. If no dependencies, use an empty array [].\n \"details\": \"<string>\",\n \"testStrategy\": \"<string>\" // Optional\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}appropriate number of{{/if}} subtasks)\n ]\n}\n\nImportant: For the 'dependencies' field, if a subtask has no dependencies, you MUST use an empty array, for example: \"dependencies\": []. Do not use null or omit the field.\n\nDo not include ANY explanatory text, markdown, or code block markers. Just the JSON object."
|
||||
"system": "You are an AI assistant with research capabilities analyzing and breaking down software development tasks.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks. Each subtask should be actionable and well-defined.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
|
||||
},
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping with task breakdown for software development.\nYou need to break down a high-level task into {{#if (gt subtaskCount 0)}}{{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks that can be implemented one by one.\n\nSubtasks should:\n1. Be specific and actionable implementation steps\n2. Follow a logical sequence\n3. Each handle a distinct part of the parent task\n4. Include clear guidance on implementation approach\n5. Have appropriate dependency chains between subtasks (using full subtask IDs)\n6. Collectively cover all aspects of the parent task\n\nFor each subtask, provide:\n- id: Sequential integer starting from the provided nextSubtaskId\n- title: Clear, specific title\n- description: Detailed description\n- dependencies: Array of prerequisite subtask IDs using full format like [\"{{task.id}}.1\", \"{{task.id}}.2\"]\n- details: Implementation details, the output should be in string\n- testStrategy: Optional testing approach\n\nRespond ONLY with a valid JSON object containing a single key \"subtasks\" whose value is an array matching the structure described. Do not include any explanatory text, markdown formatting, or code block markers.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nReturn ONLY the JSON object containing the \"subtasks\" array, matching this structure:\n\n{\n \"subtasks\": [\n {\n \"id\": {{nextSubtaskId}}, // First subtask ID\n \"title\": \"Specific subtask title\",\n \"description\": \"Detailed description\",\n \"dependencies\": [], // e.g., [\"{{task.id}}.1\", \"{{task.id}}.2\"] for dependencies. Use empty array [] if no dependencies\n \"details\": \"Implementation guidance\",\n \"testStrategy\": \"Optional testing approach\"\n },\n // ... (repeat for {{#if (gt subtaskCount 0)}}a total of {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks with sequential IDs)\n ]\n}"
|
||||
"system": "You are an AI assistant helping with task breakdown for software development. Break down high-level tasks into specific, actionable subtasks that can be implemented sequentially.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON format.{{#if research}}\nBefore breaking down the PRD into tasks, you will:\n1. Research and analyze the latest technologies, libraries, frameworks, and best practices that would be appropriate for this project\n2. Identify any potential technical challenges, security concerns, or scalability issues not explicitly mentioned in the PRD without discarding any explicit requirements or going overboard with complexity -- always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n3. Consider current industry standards and evolving trends relevant to this project (this step aims to solve LLM hallucinations and out of date information due to training data cutoff dates)\n4. Evaluate alternative implementation approaches and recommend the most efficient path\n5. Include specific library versions, helpful APIs, and concrete implementation guidance based on your research\n6. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n\nYour task breakdown should incorporate this research, resulting in more detailed implementation guidance, more accurate dependency mapping, and more precise technology recommendations than would be possible from the PRD text alone, while maintaining all explicit requirements and best practices and all details and nuances of the PRD.{{/if}}\n\nAnalyze the provided PRD content and generate {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} top-level development tasks. If the complexity or the level of detail of the PRD is high, generate more tasks relative to the complexity of the PRD\nEach task should represent a logical unit of work needed to implement the requirements and focus on the most direct and effective way to implement the requirements without unnecessary complexity or overengineering. Include pseudo-code, implementation details, and test strategy for each task. Find the most up to date information to implement each task.\nAssign sequential IDs starting from {{nextId}}. Infer title, description, details, and test strategy for each task based *only* on the PRD content.\nSet status to 'pending', dependencies to an empty array [], and priority to '{{defaultTaskPriority}}' initially for all tasks.\nRespond ONLY with a valid JSON object containing a single key \"tasks\", where the value is an array of task objects adhering to the provided Zod schema. Do not include any explanation or markdown formatting.\n\nEach task should follow this JSON structure:\n{\n\t\"id\": number,\n\t\"title\": string,\n\t\"description\": string,\n\t\"status\": \"pending\",\n\t\"dependencies\": number[] (IDs of tasks this depends on),\n\t\"priority\": \"high\" | \"medium\" | \"low\",\n\t\"details\": string (implementation details),\n\t\"testStrategy\": string (validation approach)\n}\n\nGuidelines:\n1. {{#if (gt numTasks 0)}}Unless complexity warrants otherwise{{else}}Depending on the complexity{{/if}}, create {{#if (gt numTasks 0)}}exactly {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, numbered sequentially starting from {{nextId}}\n2. Each task should be atomic and focused on a single responsibility following the most up to date best practices and standards\n3. Order tasks logically - consider dependencies and implementation sequence\n4. Early tasks should focus on setup, core functionality first, then advanced features\n5. Include clear validation/testing approach for each task\n6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs, potentially including existing tasks with IDs less than {{nextId}} if applicable)\n7. Assign priority (high/medium/low) based on criticality and dependency order\n8. Include detailed implementation guidance in the \"details\" field{{#if research}}, with specific libraries and version recommendations based on your research{{/if}}\n9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance\n10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements\n11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches{{#if research}}\n12. For each task, include specific, actionable guidance based on current industry standards and best practices discovered through research{{/if}}",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, README.md, and main entry points\n4. Analyze the current state of implementation to understand what already exists\n\nBased on your analysis:\n- Identify what components/features are already implemented\n- Understand the technology stack, frameworks, and patterns in use\n- Generate tasks that build upon the existing codebase rather than duplicating work\n- Ensure tasks align with the project's current architecture and conventions\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here's the Product Requirements Document (PRD) to break down into {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, starting IDs from {{nextId}}:{{#if research}}\n\nRemember to thoroughly research current best practices and technologies before task breakdown to provide specific, actionable implementation details.{{/if}}\n\n{{prdContent}}\n\n\n\t\tReturn your response in this format:\n{\n \"tasks\": [\n {\n \"id\": 1,\n \"title\": \"Setup Project Repository\",\n \"description\": \"...\",\n ...\n },\n ...\n ],\n \"metadata\": {\n \"projectName\": \"PRD Implementation\",\n \"totalTasks\": {{#if (gt numTasks 0)}}{{numTasks}}{{else}}{number of tasks}{{/if}},\n \"sourceFile\": \"{{prdPath}}\",\n \"generatedAt\": \"YYYY-MM-DD\"\n }\n}"
|
||||
"system": "You are an AI assistant specialized in analyzing Product Requirements Documents (PRDs) and generating a structured, logically ordered, dependency-aware and sequenced list of development tasks in JSON format.{{#if research}}\nBefore breaking down the PRD into tasks, you will:\n1. Research and analyze the latest technologies, libraries, frameworks, and best practices that would be appropriate for this project\n2. Identify any potential technical challenges, security concerns, or scalability issues not explicitly mentioned in the PRD without discarding any explicit requirements or going overboard with complexity -- always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n3. Consider current industry standards and evolving trends relevant to this project (this step aims to solve LLM hallucinations and out of date information due to training data cutoff dates)\n4. Evaluate alternative implementation approaches and recommend the most efficient path\n5. Include specific library versions, helpful APIs, and concrete implementation guidance based on your research\n6. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches\n\nYour task breakdown should incorporate this research, resulting in more detailed implementation guidance, more accurate dependency mapping, and more precise technology recommendations than would be possible from the PRD text alone, while maintaining all explicit requirements and best practices and all details and nuances of the PRD.{{/if}}\n\nAnalyze the provided PRD content and generate {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} top-level development tasks. If the complexity or the level of detail of the PRD is high, generate more tasks relative to the complexity of the PRD\nEach task should represent a logical unit of work needed to implement the requirements and focus on the most direct and effective way to implement the requirements without unnecessary complexity or overengineering. Include pseudo-code, implementation details, and test strategy for each task. Find the most up to date information to implement each task.\nAssign sequential IDs starting from {{nextId}}. Infer title, description, details, and test strategy for each task based *only* on the PRD content.\nSet status to 'pending', dependencies to an empty array [], and priority to '{{defaultTaskPriority}}' initially for all tasks.\nGenerate a response containing a single key \"tasks\", where the value is an array of task objects adhering to the provided schema.\n\nEach task should follow this JSON structure:\n{\n\t\"id\": number,\n\t\"title\": string,\n\t\"description\": string,\n\t\"status\": \"pending\",\n\t\"dependencies\": number[] (IDs of tasks this depends on),\n\t\"priority\": \"high\" | \"medium\" | \"low\",\n\t\"details\": string (implementation details),\n\t\"testStrategy\": string (validation approach)\n}\n\nGuidelines:\n1. {{#if (gt numTasks 0)}}Unless complexity warrants otherwise{{else}}Depending on the complexity{{/if}}, create {{#if (gt numTasks 0)}}exactly {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, numbered sequentially starting from {{nextId}}\n2. Each task should be atomic and focused on a single responsibility following the most up to date best practices and standards\n3. Order tasks logically - consider dependencies and implementation sequence\n4. Early tasks should focus on setup, core functionality first, then advanced features\n5. Include clear validation/testing approach for each task\n6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs, potentially including existing tasks with IDs less than {{nextId}} if applicable)\n7. Assign priority (high/medium/low) based on criticality and dependency order\n8. Include detailed implementation guidance in the \"details\" field{{#if research}}, with specific libraries and version recommendations based on your research{{/if}}\n9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance\n10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements\n11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches{{#if research}}\n12. For each task, include specific, actionable guidance based on current industry standards and best practices discovered through research{{/if}}",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, README.md, and main entry points\n4. Analyze the current state of implementation to understand what already exists\n\nBased on your analysis:\n- Identify what components/features are already implemented\n- Understand the technology stack, frameworks, and patterns in use\n- Generate tasks that build upon the existing codebase rather than duplicating work\n- Ensure tasks align with the project's current architecture and conventions\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here's the Product Requirements Document (PRD) to break down into {{#if (gt numTasks 0)}}approximately {{numTasks}}{{else}}an appropriate number of{{/if}} tasks, starting IDs from {{nextId}}:{{#if research}}\n\nRemember to thoroughly research current best practices and technologies before task breakdown to provide specific, actionable implementation details.{{/if}}\n\n{{prdContent}}\n\nIMPORTANT: Your response must be a JSON object with a single property named \"tasks\" containing an array of task objects. Do NOT include metadata or any other properties."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update a software development task based on new context.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\nYou will be given a task and a prompt describing changes or new implementation details.\nYour job is to update the task to reflect these changes, while preserving its basic structure.\n\nGuidelines:\n1. VERY IMPORTANT: NEVER change the title of the task - keep it exactly as is\n2. Maintain the same ID, status, and dependencies unless specifically mentioned in the prompt{{#if useResearch}}\n3. Research and update the description, details, and test strategy with current best practices\n4. Include specific versions, libraries, and approaches that are current and well-tested{{/if}}{{#if (not useResearch)}}\n3. Update the description, details, and test strategy to reflect the new information\n4. Do not change anything unnecessarily - just adapt what needs to change based on the prompt{{/if}}\n5. Return a complete valid JSON object representing the updated task\n6. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n7. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n8. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n11. Ensure any new subtasks have unique IDs that don't conflict with existing ones\n12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings (\"1\", \"2\", \"3\")\n13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix{{#if useResearch}}\n14. Include links to documentation or resources where helpful\n15. Focus on practical, implementable solutions using current technologies{{/if}}\n\nThe changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the task changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}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}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn only the updated task as a valid JSON object{{#if useResearch}} with research-backed improvements{{/if}}."
|
||||
"system": "You are an AI assistant helping to update a software development task based on new context.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\nYou will be given a task and a prompt describing changes or new implementation details.\nYour job is to update the task to reflect these changes, while preserving its basic structure.\n\nGuidelines:\n1. VERY IMPORTANT: NEVER change the title of the task - keep it exactly as is\n2. Maintain the same ID, status, and dependencies unless specifically mentioned in the prompt{{#if useResearch}}\n3. Research and update the description, details, and test strategy with current best practices\n4. Include specific versions, libraries, and approaches that are current and well-tested{{/if}}{{#if (not useResearch)}}\n3. Update the description, details, and test strategy to reflect the new information\n4. Do not change anything unnecessarily - just adapt what needs to change based on the prompt{{/if}}\n5. Return the complete updated task\n6. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n7. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n8. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n11. Ensure any new subtasks have unique IDs that don't conflict with existing ones\n12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings (\"1\", \"2\", \"3\")\n13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix{{#if useResearch}}\n14. Include links to documentation or resources where helpful\n15. Focus on practical, implementable solutions using current technologies{{/if}}\n\nThe changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the task changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here is the task to update{{#if useResearch}} with research-backed information{{/if}}:\n{{{taskJson}}}\n\nPlease {{#if useResearch}}research and {{/if}}update this task based on the following {{#if useResearch}}context:\n{{updatePrompt}}\n\nIncorporate current best practices, latest stable versions, and proven approaches.{{/if}}{{#if (not useResearch)}}new context:\n{{updatePrompt}}{{/if}}\n\nIMPORTANT: {{#if useResearch}}Preserve any subtasks marked as \"done\" or \"completed\".{{/if}}{{#if (not useResearch)}}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}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\nReturn the complete updated task{{#if useResearch}} with research-backed improvements{{/if}}.\n\nIMPORTANT: Your response must be a JSON object with a single property named \"task\" containing the updated task object."
|
||||
},
|
||||
"append": {
|
||||
"condition": "appendMode === true",
|
||||
"system": "You are an AI assistant helping to append additional information to a software development task. You will be provided with the task's existing details, context, and a user request string.\n\nYour Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the task's details.\nFocus *only* on generating the substance of the update.\n\nOutput Requirements:\n1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.\n2. Your string response should NOT include any of the task's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.\n3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.\n4. 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...\").",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return ONLY this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nTask: {{{json task}}}\nCurrent Task Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\nBased on the User Request and all the Task Context (including current task details provided above), what is the new information or text that should be appended to this task's details? Return this new text as a plain string.\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
},
|
||||
"prompts": {
|
||||
"default": {
|
||||
"system": "You are an AI assistant helping to update software development tasks based on new context.\nYou will be given a set of tasks and a prompt describing changes or new implementation details.\nYour job is to update the tasks to reflect these changes, while preserving their basic structure.\n\nCRITICAL RULES:\n1. Return ONLY a JSON array - no explanations, no markdown, no additional text before or after\n2. Each task MUST have ALL fields from the original (do not omit any fields)\n3. Maintain the same IDs, statuses, and dependencies unless specifically mentioned in the prompt\n4. Update titles, descriptions, details, and test strategies to reflect the new information\n5. Do not change anything unnecessarily - just adapt what needs to change based on the prompt\n6. You should return ALL the tasks in order, not just the modified ones\n7. Return a complete valid JSON array with all tasks\n8. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n9. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n10. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n11. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n12. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n\nThe changes described in the prompt should be applied to ALL tasks in the list.",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the new changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\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 projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nRequired JSON structure for EACH task (ALL fields MUST be present):\n{\n \"id\": <number>,\n \"title\": <string>,\n \"description\": <string>,\n \"status\": <string>,\n \"dependencies\": <array>,\n \"priority\": <string or null>,\n \"details\": <string or null>,\n \"testStrategy\": <string or null>,\n \"subtasks\": <array or null>\n}\n\nReturn a valid JSON array containing ALL the tasks with ALL their fields:\n- id (number) - preserve existing value\n- title (string)\n- description (string)\n- status (string) - preserve existing value unless explicitly changing\n- dependencies (array) - preserve existing value unless explicitly changing\n- priority (string or null)\n- details (string or null)\n- testStrategy (string or null)\n- subtasks (array or null)\n\nReturn ONLY the JSON array now:"
|
||||
"system": "You are an AI assistant helping to update software development tasks based on new context.\nYou will be given a set of tasks and a prompt describing changes or new implementation details.\nYour job is to update the tasks to reflect these changes, while preserving their basic structure.\n\nGuidelines:\n1. Maintain the same IDs, statuses, and dependencies unless specifically mentioned in the prompt\n2. Update titles, descriptions, details, and test strategies to reflect the new information\n3. Do not change anything unnecessarily - just adapt what needs to change based on the prompt\n4. Return ALL the tasks in order, not just the modified ones\n5. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n6. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n7. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n8. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n9. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted",
|
||||
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the new changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\n\nIMPORTANT: In the tasks above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{#if projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nIMPORTANT: Your response must be a JSON object with a single property named \"tasks\" containing the updated array of tasks."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/schemas/add-task.js
Normal file
21
src/schemas/add-task.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema that matches the inline AiTaskDataSchema from add-task.js
|
||||
export const AddTaskResponseSchema = z.object({
|
||||
title: z.string().describe('Clear, concise title for the task'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('A one or two sentence description of the task'),
|
||||
details: z
|
||||
.string()
|
||||
.describe('In-depth implementation details, considerations, and guidance'),
|
||||
testStrategy: z
|
||||
.string()
|
||||
.describe('Detailed approach for verifying task completion'),
|
||||
dependencies: z
|
||||
.array(z.number())
|
||||
.nullable()
|
||||
.describe(
|
||||
'Array of task IDs that this task depends on (must be completed before this task can start)'
|
||||
)
|
||||
});
|
||||
14
src/schemas/analyze-complexity.js
Normal file
14
src/schemas/analyze-complexity.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ComplexityAnalysisItemSchema = z.object({
|
||||
taskId: z.number().int().positive(),
|
||||
taskTitle: z.string(),
|
||||
complexityScore: z.number().min(1).max(10),
|
||||
recommendedSubtasks: z.number().int().nonnegative(),
|
||||
expansionPrompt: z.string(),
|
||||
reasoning: z.string()
|
||||
});
|
||||
|
||||
export const ComplexityAnalysisResponseSchema = z.object({
|
||||
complexityAnalysis: z.array(ComplexityAnalysisItemSchema)
|
||||
});
|
||||
35
src/schemas/base-schemas.js
Normal file
35
src/schemas/base-schemas.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Base schemas that will be reused across commands
|
||||
export const TaskStatusSchema = z.enum([
|
||||
'pending',
|
||||
'in-progress',
|
||||
'blocked',
|
||||
'done',
|
||||
'cancelled',
|
||||
'deferred'
|
||||
]);
|
||||
|
||||
export const BaseTaskSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
title: z.string().min(1).max(200),
|
||||
description: z.string().min(1),
|
||||
status: TaskStatusSchema,
|
||||
dependencies: z.array(z.union([z.number().int(), z.string()])).default([]),
|
||||
priority: z
|
||||
.enum(['low', 'medium', 'high', 'critical'])
|
||||
.nullable()
|
||||
.default(null),
|
||||
details: z.string().nullable().default(null),
|
||||
testStrategy: z.string().nullable().default(null)
|
||||
});
|
||||
|
||||
export const SubtaskSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
title: z.string().min(5).max(200),
|
||||
description: z.string().min(10),
|
||||
dependencies: z.array(z.number().int()).default([]),
|
||||
details: z.string().min(20),
|
||||
status: z.enum(['pending', 'done', 'completed']).default('pending'),
|
||||
testStrategy: z.string().nullable().default(null)
|
||||
});
|
||||
6
src/schemas/expand-task.js
Normal file
6
src/schemas/expand-task.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { SubtaskSchema } from './base-schemas.js';
|
||||
|
||||
export const ExpandTaskResponseSchema = z.object({
|
||||
subtasks: z.array(SubtaskSchema)
|
||||
});
|
||||
18
src/schemas/parse-prd.js
Normal file
18
src/schemas/parse-prd.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema for a single task from PRD parsing
|
||||
const PRDSingleTaskSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
details: z.string().nullable(),
|
||||
testStrategy: z.string().nullable(),
|
||||
priority: z.enum(['high', 'medium', 'low']).nullable(),
|
||||
dependencies: z.array(z.number().int().positive()).nullable(),
|
||||
status: z.string().nullable()
|
||||
});
|
||||
|
||||
// Schema for the AI response - only expects tasks array since metadata is generated by the code
|
||||
export const ParsePRDResponseSchema = z.object({
|
||||
tasks: z.array(PRDSingleTaskSchema)
|
||||
});
|
||||
27
src/schemas/registry.js
Normal file
27
src/schemas/registry.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { AddTaskResponseSchema } from './add-task.js';
|
||||
import { ComplexityAnalysisResponseSchema } from './analyze-complexity.js';
|
||||
import { ExpandTaskResponseSchema } from './expand-task.js';
|
||||
import { ParsePRDResponseSchema } from './parse-prd.js';
|
||||
import { UpdateSubtaskResponseSchema } from './update-subtask.js';
|
||||
import { UpdateTaskResponseSchema } from './update-task.js';
|
||||
import { UpdateTasksResponseSchema } from './update-tasks.js';
|
||||
|
||||
export const COMMAND_SCHEMAS = {
|
||||
'update-tasks': UpdateTasksResponseSchema,
|
||||
'expand-task': ExpandTaskResponseSchema,
|
||||
'analyze-complexity': ComplexityAnalysisResponseSchema,
|
||||
'update-subtask-by-id': UpdateSubtaskResponseSchema,
|
||||
'update-task-by-id': UpdateTaskResponseSchema,
|
||||
'add-task': AddTaskResponseSchema,
|
||||
'parse-prd': ParsePRDResponseSchema
|
||||
};
|
||||
|
||||
// Export individual schemas for direct access
|
||||
export * from './update-tasks.js';
|
||||
export * from './expand-task.js';
|
||||
export * from './analyze-complexity.js';
|
||||
export * from './update-subtask.js';
|
||||
export * from './update-task.js';
|
||||
export * from './add-task.js';
|
||||
export * from './parse-prd.js';
|
||||
export * from './base-schemas.js';
|
||||
6
src/schemas/update-subtask.js
Normal file
6
src/schemas/update-subtask.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { SubtaskSchema } from './base-schemas.js';
|
||||
|
||||
export const UpdateSubtaskResponseSchema = z.object({
|
||||
subtask: SubtaskSchema
|
||||
});
|
||||
6
src/schemas/update-task.js
Normal file
6
src/schemas/update-task.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { UpdatedTaskSchema } from './update-tasks.js';
|
||||
|
||||
export const UpdateTaskResponseSchema = z.object({
|
||||
task: UpdatedTaskSchema
|
||||
});
|
||||
10
src/schemas/update-tasks.js
Normal file
10
src/schemas/update-tasks.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { BaseTaskSchema, SubtaskSchema } from './base-schemas.js';
|
||||
|
||||
export const UpdatedTaskSchema = BaseTaskSchema.extend({
|
||||
subtasks: z.array(SubtaskSchema).nullable().default(null)
|
||||
});
|
||||
|
||||
export const UpdateTasksResponseSchema = z.object({
|
||||
tasks: z.array(UpdatedTaskSchema)
|
||||
});
|
||||
@@ -330,7 +330,7 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
|
||||
describe('Large Task Set Performance', () => {
|
||||
it('should handle large task sets efficiently', () => {
|
||||
// Create a large task set (100 tasks)
|
||||
// Create a large task set (50 tasks)
|
||||
const largeTaskSet = {
|
||||
master: {
|
||||
tasks: [],
|
||||
@@ -348,8 +348,8 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Add 50 tasks to master with dependencies
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
// Add 25 tasks to master with dependencies
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
largeTaskSet.master.tasks.push({
|
||||
id: i,
|
||||
title: `Task ${i}`,
|
||||
@@ -359,8 +359,8 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Add 50 tasks to in-progress
|
||||
for (let i = 51; i <= 100; i++) {
|
||||
// Add 25 tasks to in-progress (ensure no ID conflict with master)
|
||||
for (let i = 26; i <= 50; i++) {
|
||||
largeTaskSet['in-progress'].tasks.push({
|
||||
id: i,
|
||||
title: `Task ${i}`,
|
||||
@@ -371,21 +371,32 @@ describe('Complex Cross-Tag Scenarios', () => {
|
||||
}
|
||||
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(largeTaskSet, null, 2));
|
||||
// Should complete within reasonable time
|
||||
const timeout = process.env.CI ? 12000 : 8000;
|
||||
const startTime = Date.now();
|
||||
// Execute move; correctness is validated below (no timing assertion)
|
||||
execSync(
|
||||
`node ${binPath} move --from=50 --from-tag=master --to-tag=in-progress --with-dependencies`,
|
||||
`node ${binPath} move --from=25 --from-tag=master --to-tag=in-progress --with-dependencies`,
|
||||
{ stdio: 'pipe' }
|
||||
);
|
||||
const endTime = Date.now();
|
||||
expect(endTime - startTime).toBeLessThan(timeout);
|
||||
|
||||
// Verify the move was successful
|
||||
const tasksAfter = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
|
||||
expect(
|
||||
tasksAfter['in-progress'].tasks.find((t) => t.id === 50)
|
||||
).toBeDefined();
|
||||
|
||||
// Verify all tasks in the dependency chain were moved
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
expect(tasksAfter.master.tasks.find((t) => t.id === i)).toBeUndefined();
|
||||
expect(
|
||||
tasksAfter['in-progress'].tasks.find((t) => t.id === i)
|
||||
).toBeDefined();
|
||||
}
|
||||
|
||||
// Verify in-progress still has its original tasks (26-50)
|
||||
for (let i = 26; i <= 50; i++) {
|
||||
expect(
|
||||
tasksAfter['in-progress'].tasks.find((t) => t.id === i)
|
||||
).toBeDefined();
|
||||
}
|
||||
|
||||
// Final count check
|
||||
expect(tasksAfter['in-progress'].tasks).toHaveLength(50); // 25 moved + 25 original
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
62
tests/integration/providers/temperature-support.test.js
Normal file
62
tests/integration/providers/temperature-support.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Integration Tests for Provider Temperature Support
|
||||
*
|
||||
* This test suite verifies that all providers correctly declare their
|
||||
* temperature support capabilities. CLI providers should have
|
||||
* supportsTemperature = false, while standard API providers should
|
||||
* have supportsTemperature = true.
|
||||
*
|
||||
* These tests are separated from unit tests to avoid coupling
|
||||
* base provider tests with concrete provider implementations.
|
||||
*/
|
||||
|
||||
import { ClaudeCodeProvider } from '../../../src/ai-providers/claude-code.js';
|
||||
import { CodexCliProvider } from '../../../src/ai-providers/codex-cli.js';
|
||||
import { GeminiCliProvider } from '../../../src/ai-providers/gemini-cli.js';
|
||||
import { GrokCliProvider } from '../../../src/ai-providers/grok-cli.js';
|
||||
import { AnthropicAIProvider } from '../../../src/ai-providers/anthropic.js';
|
||||
import { OpenAIProvider } from '../../../src/ai-providers/openai.js';
|
||||
import { GoogleAIProvider } from '../../../src/ai-providers/google.js';
|
||||
import { PerplexityAIProvider } from '../../../src/ai-providers/perplexity.js';
|
||||
import { XAIProvider } from '../../../src/ai-providers/xai.js';
|
||||
import { GroqProvider } from '../../../src/ai-providers/groq.js';
|
||||
import { OpenRouterAIProvider } from '../../../src/ai-providers/openrouter.js';
|
||||
import { OllamaAIProvider } from '../../../src/ai-providers/ollama.js';
|
||||
import { BedrockAIProvider } from '../../../src/ai-providers/bedrock.js';
|
||||
import { AzureProvider } from '../../../src/ai-providers/azure.js';
|
||||
import { VertexAIProvider } from '../../../src/ai-providers/google-vertex.js';
|
||||
|
||||
describe('Provider Temperature Support', () => {
|
||||
describe('CLI Providers', () => {
|
||||
it('should verify CLI providers have supportsTemperature = false', () => {
|
||||
expect(new ClaudeCodeProvider().supportsTemperature).toBe(false);
|
||||
expect(new CodexCliProvider().supportsTemperature).toBe(false);
|
||||
expect(new GeminiCliProvider().supportsTemperature).toBe(false);
|
||||
expect(new GrokCliProvider().supportsTemperature).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Standard API Providers', () => {
|
||||
it('should verify standard providers have supportsTemperature = true', () => {
|
||||
expect(new AnthropicAIProvider().supportsTemperature).toBe(true);
|
||||
expect(new OpenAIProvider().supportsTemperature).toBe(true);
|
||||
expect(new GoogleAIProvider().supportsTemperature).toBe(true);
|
||||
expect(new PerplexityAIProvider().supportsTemperature).toBe(true);
|
||||
expect(new XAIProvider().supportsTemperature).toBe(true);
|
||||
expect(new GroqProvider().supportsTemperature).toBe(true);
|
||||
expect(new OpenRouterAIProvider().supportsTemperature).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special Case Providers', () => {
|
||||
it('should verify Ollama provider has supportsTemperature = true', () => {
|
||||
expect(new OllamaAIProvider().supportsTemperature).toBe(true);
|
||||
});
|
||||
|
||||
it('should verify cloud providers have supportsTemperature = true', () => {
|
||||
expect(new BedrockAIProvider().supportsTemperature).toBe(true);
|
||||
expect(new AzureProvider().supportsTemperature).toBe(true);
|
||||
expect(new VertexAIProvider().supportsTemperature).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
669
tests/unit/ai-providers/base-provider.test.js
Normal file
669
tests/unit/ai-providers/base-provider.test.js
Normal file
@@ -0,0 +1,669 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock the 'ai' SDK
|
||||
const mockGenerateText = jest.fn();
|
||||
const mockGenerateObject = jest.fn();
|
||||
const mockNoObjectGeneratedError = class NoObjectGeneratedError extends Error {
|
||||
static isInstance(error) {
|
||||
return error instanceof mockNoObjectGeneratedError;
|
||||
}
|
||||
constructor(cause) {
|
||||
super('No object generated');
|
||||
this.cause = cause;
|
||||
this.usage = cause.usage;
|
||||
}
|
||||
};
|
||||
const mockJSONParseError = class JSONParseError extends Error {
|
||||
constructor(text) {
|
||||
super('JSON parse error');
|
||||
this.text = text;
|
||||
}
|
||||
};
|
||||
|
||||
jest.unstable_mockModule('ai', () => ({
|
||||
generateText: mockGenerateText,
|
||||
streamText: jest.fn(),
|
||||
generateObject: mockGenerateObject,
|
||||
streamObject: jest.fn(),
|
||||
zodSchema: jest.fn((schema) => schema),
|
||||
NoObjectGeneratedError: mockNoObjectGeneratedError,
|
||||
JSONParseError: mockJSONParseError
|
||||
}));
|
||||
|
||||
// Mock jsonrepair
|
||||
const mockJsonrepair = jest.fn();
|
||||
jest.unstable_mockModule('jsonrepair', () => ({
|
||||
jsonrepair: mockJsonrepair
|
||||
}));
|
||||
|
||||
// Mock logging and utilities
|
||||
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
||||
log: jest.fn(),
|
||||
findProjectRoot: jest.fn(() => '/mock/project/root'),
|
||||
isEmpty: jest.fn(
|
||||
(val) =>
|
||||
!val ||
|
||||
(Array.isArray(val) && val.length === 0) ||
|
||||
(typeof val === 'object' && Object.keys(val).length === 0)
|
||||
),
|
||||
resolveEnvVariable: jest.fn((key) => process.env[key])
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
const { BaseAIProvider } = await import(
|
||||
'../../../src/ai-providers/base-provider.js'
|
||||
);
|
||||
|
||||
describe('BaseAIProvider', () => {
|
||||
let testProvider;
|
||||
let mockClient;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a concrete test provider
|
||||
class TestProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'TestProvider';
|
||||
}
|
||||
|
||||
getRequiredApiKeyName() {
|
||||
return 'TEST_API_KEY';
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
return mockClient;
|
||||
}
|
||||
}
|
||||
|
||||
mockClient = jest.fn((modelId) => ({ modelId }));
|
||||
jest.clearAllMocks();
|
||||
testProvider = new TestProvider();
|
||||
});
|
||||
|
||||
describe('1. Parameter Validation - Catches Invalid Inputs', () => {
|
||||
describe('validateAuth', () => {
|
||||
it('should throw when API key is missing', () => {
|
||||
expect(() => testProvider.validateAuth({})).toThrow(
|
||||
'TestProvider API key is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass when API key is provided', () => {
|
||||
expect(() =>
|
||||
testProvider.validateAuth({ apiKey: 'test-key' })
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateParams', () => {
|
||||
it('should throw when model ID is missing', () => {
|
||||
expect(() => testProvider.validateParams({ apiKey: 'key' })).toThrow(
|
||||
'TestProvider Model ID is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when both API key and model ID are missing', () => {
|
||||
expect(() => testProvider.validateParams({})).toThrow(
|
||||
'TestProvider API key is required'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOptionalParams', () => {
|
||||
it('should throw for temperature below 0', () => {
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ temperature: -0.1 })
|
||||
).toThrow('Temperature must be between 0 and 1');
|
||||
});
|
||||
|
||||
it('should throw for temperature above 1', () => {
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ temperature: 1.1 })
|
||||
).toThrow('Temperature must be between 0 and 1');
|
||||
});
|
||||
|
||||
it('should accept temperature at boundaries', () => {
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ temperature: 0 })
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ temperature: 1 })
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for invalid maxTokens values', () => {
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ maxTokens: 0 })
|
||||
).toThrow('maxTokens must be a finite number greater than 0');
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ maxTokens: -100 })
|
||||
).toThrow('maxTokens must be a finite number greater than 0');
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ maxTokens: Infinity })
|
||||
).toThrow('maxTokens must be a finite number greater than 0');
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ maxTokens: 'invalid' })
|
||||
).toThrow('maxTokens must be a finite number greater than 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMessages', () => {
|
||||
it('should throw for null/undefined messages', async () => {
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: null
|
||||
})
|
||||
).rejects.toThrow('Invalid or empty messages array provided');
|
||||
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: undefined
|
||||
})
|
||||
).rejects.toThrow('Invalid or empty messages array provided');
|
||||
});
|
||||
|
||||
it('should throw for empty messages array', async () => {
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: []
|
||||
})
|
||||
).rejects.toThrow('Invalid or empty messages array provided');
|
||||
});
|
||||
|
||||
it('should throw for messages without role or content', async () => {
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ content: 'test' }] // missing role
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Invalid message format. Each message must have role and content'
|
||||
);
|
||||
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user' }] // missing content
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Invalid message format. Each message must have role and content'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. Error Handling - Proper Error Context', () => {
|
||||
it('should wrap API errors with context', async () => {
|
||||
const apiError = new Error('API rate limit exceeded');
|
||||
mockGenerateText.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'TestProvider API error during text generation: API rate limit exceeded'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors without message property', async () => {
|
||||
const apiError = { code: 'NETWORK_ERROR' };
|
||||
mockGenerateText.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'TestProvider API error during text generation: Unknown error occurred'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. Abstract Class Protection', () => {
|
||||
it('should prevent direct instantiation of BaseAIProvider', () => {
|
||||
expect(() => new BaseAIProvider()).toThrow(
|
||||
'BaseAIProvider cannot be instantiated directly'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when abstract methods are not implemented', () => {
|
||||
class IncompleteProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
const provider = new IncompleteProvider();
|
||||
|
||||
expect(() => provider.getClient()).toThrow(
|
||||
'getClient must be implemented by provider'
|
||||
);
|
||||
expect(() => provider.getRequiredApiKeyName()).toThrow(
|
||||
'getRequiredApiKeyName must be implemented by provider'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. Token Parameter Preparation', () => {
|
||||
it('should convert maxTokens to maxOutputTokens as integer', () => {
|
||||
const result = testProvider.prepareTokenParam('model', 1000.7);
|
||||
expect(result).toEqual({ maxOutputTokens: 1000 });
|
||||
});
|
||||
|
||||
it('should handle string numbers', () => {
|
||||
const result = testProvider.prepareTokenParam('model', '500');
|
||||
expect(result).toEqual({ maxOutputTokens: 500 });
|
||||
});
|
||||
|
||||
it('should return empty object when maxTokens is undefined', () => {
|
||||
const result = testProvider.prepareTokenParam('model', undefined);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should floor decimal values', () => {
|
||||
const result = testProvider.prepareTokenParam('model', 999.99);
|
||||
expect(result).toEqual({ maxOutputTokens: 999 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. JSON Repair for Malformed Responses', () => {
|
||||
it('should repair malformed JSON in generateObject errors', async () => {
|
||||
const malformedJson = '{"key": "value",,}'; // Double comma
|
||||
const repairedJson = '{"key": "value"}';
|
||||
|
||||
const parseError = new mockJSONParseError(malformedJson);
|
||||
const noObjectError = new mockNoObjectGeneratedError(parseError);
|
||||
noObjectError.usage = {
|
||||
promptTokens: 100,
|
||||
completionTokens: 50,
|
||||
totalTokens: 150
|
||||
};
|
||||
|
||||
mockGenerateObject.mockRejectedValue(noObjectError);
|
||||
mockJsonrepair.mockReturnValue(repairedJson);
|
||||
|
||||
const result = await testProvider.generateObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
schema: { type: 'object' },
|
||||
objectName: 'TestObject'
|
||||
});
|
||||
|
||||
expect(mockJsonrepair).toHaveBeenCalledWith(malformedJson);
|
||||
expect(result).toEqual({
|
||||
object: { key: 'value' },
|
||||
usage: {
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
totalTokens: 150
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw original error when JSON repair fails', async () => {
|
||||
const malformedJson = 'not even close to JSON';
|
||||
const parseError = new mockJSONParseError(malformedJson);
|
||||
const noObjectError = new mockNoObjectGeneratedError(parseError);
|
||||
|
||||
mockGenerateObject.mockRejectedValue(noObjectError);
|
||||
mockJsonrepair.mockImplementation(() => {
|
||||
throw new Error('Cannot repair this JSON');
|
||||
});
|
||||
|
||||
await expect(
|
||||
testProvider.generateObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
schema: { type: 'object' },
|
||||
objectName: 'TestObject'
|
||||
})
|
||||
).rejects.toThrow('TestProvider API error during object generation');
|
||||
});
|
||||
|
||||
it('should handle non-JSON parse errors normally', async () => {
|
||||
const regularError = new Error('Network timeout');
|
||||
mockGenerateObject.mockRejectedValue(regularError);
|
||||
|
||||
await expect(
|
||||
testProvider.generateObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
schema: { type: 'object' },
|
||||
objectName: 'TestObject'
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'TestProvider API error during object generation: Network timeout'
|
||||
);
|
||||
|
||||
expect(mockJsonrepair).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('6. Usage Token Normalization', () => {
|
||||
it('should normalize different token formats in generateText', async () => {
|
||||
// Test promptTokens/completionTokens format (older format)
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { promptTokens: 10, completionTokens: 5 }
|
||||
});
|
||||
|
||||
let result = await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
});
|
||||
|
||||
expect(result.usage).toEqual({
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
totalTokens: 15
|
||||
});
|
||||
|
||||
// Test inputTokens/outputTokens format (newer format)
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 }
|
||||
});
|
||||
|
||||
result = await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
});
|
||||
|
||||
expect(result.usage).toEqual({
|
||||
inputTokens: 20,
|
||||
outputTokens: 10,
|
||||
totalTokens: 30
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing usage data gracefully', async () => {
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: undefined
|
||||
});
|
||||
|
||||
const result = await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
});
|
||||
|
||||
expect(result.usage).toEqual({
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate totalTokens when missing', async () => {
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 15, outputTokens: 25 }
|
||||
});
|
||||
|
||||
const result = await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
});
|
||||
|
||||
expect(result.usage.totalTokens).toBe(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('7. Schema Validation for Object Methods', () => {
|
||||
it('should throw when schema is missing for generateObject', async () => {
|
||||
await expect(
|
||||
testProvider.generateObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
objectName: 'TestObject'
|
||||
// missing schema
|
||||
})
|
||||
).rejects.toThrow('Schema is required for object generation');
|
||||
});
|
||||
|
||||
it('should throw when objectName is missing for generateObject', async () => {
|
||||
await expect(
|
||||
testProvider.generateObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
schema: { type: 'object' }
|
||||
// missing objectName
|
||||
})
|
||||
).rejects.toThrow('Object name is required for object generation');
|
||||
});
|
||||
|
||||
it('should throw when schema is missing for streamObject', async () => {
|
||||
await expect(
|
||||
testProvider.streamObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
// missing schema
|
||||
})
|
||||
).rejects.toThrow('Schema is required for object streaming');
|
||||
});
|
||||
|
||||
it('should use json mode when needsExplicitJsonSchema is true', async () => {
|
||||
testProvider.needsExplicitJsonSchema = true;
|
||||
mockGenerateObject.mockResolvedValue({
|
||||
object: { test: 'value' },
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
await testProvider.generateObject({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
schema: { type: 'object' },
|
||||
objectName: 'TestObject'
|
||||
});
|
||||
|
||||
expect(mockGenerateObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: 'json' // Should be 'json' not 'auto'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('8. Integration Points - Client Creation', () => {
|
||||
it('should pass params to getClient method', async () => {
|
||||
const getClientSpy = jest.spyOn(testProvider, 'getClient');
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
const params = {
|
||||
apiKey: 'test-key',
|
||||
modelId: 'test-model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
customParam: 'custom-value'
|
||||
};
|
||||
|
||||
await testProvider.generateText(params);
|
||||
|
||||
expect(getClientSpy).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('should use client with correct model ID', async () => {
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'gpt-4-turbo',
|
||||
messages: [{ role: 'user', content: 'test' }]
|
||||
});
|
||||
|
||||
expect(mockClient).toHaveBeenCalledWith('gpt-4-turbo');
|
||||
expect(mockGenerateText).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: { modelId: 'gpt-4-turbo' }
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('9. Edge Cases - Boundary Conditions', () => {
|
||||
it('should handle zero maxTokens gracefully', () => {
|
||||
// This should throw in validation
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ maxTokens: 0 })
|
||||
).toThrow('maxTokens must be a finite number greater than 0');
|
||||
});
|
||||
|
||||
it('should handle very large maxTokens', () => {
|
||||
const result = testProvider.prepareTokenParam('model', 999999999);
|
||||
expect(result).toEqual({ maxOutputTokens: 999999999 });
|
||||
});
|
||||
|
||||
it('should handle NaN temperature gracefully', () => {
|
||||
// NaN fails the range check (NaN < 0 is false, NaN > 1 is also false)
|
||||
// But NaN is not between 0 and 1, so we need to check the actual behavior
|
||||
// The current implementation doesn't explicitly check for NaN,
|
||||
// it passes because NaN < 0 and NaN > 1 are both false
|
||||
expect(() =>
|
||||
testProvider.validateOptionalParams({ temperature: NaN })
|
||||
).not.toThrow();
|
||||
// This is actually a bug - NaN should be rejected
|
||||
// But we're testing current behavior, not desired behavior
|
||||
});
|
||||
|
||||
it('should handle concurrent calls safely', async () => {
|
||||
mockGenerateText.mockImplementation(async () => ({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
}));
|
||||
|
||||
const promises = Array.from({ length: 10 }, (_, i) =>
|
||||
testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: `model-${i}`,
|
||||
messages: [{ role: 'user', content: `test-${i}` }]
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
expect(results).toHaveLength(10);
|
||||
expect(mockClient).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('10. Default Behavior - isRequiredApiKey', () => {
|
||||
it('should return true by default for isRequiredApiKey', () => {
|
||||
expect(testProvider.isRequiredApiKey()).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow override of isRequiredApiKey', () => {
|
||||
class NoAuthProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
validateAuth() {
|
||||
// Override to not require API key
|
||||
}
|
||||
getClient() {
|
||||
return mockClient;
|
||||
}
|
||||
getRequiredApiKeyName() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new NoAuthProvider();
|
||||
expect(provider.isRequiredApiKey()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('11. Temperature Filtering - CLI vs Standard Providers', () => {
|
||||
const mockStreamText = jest.fn();
|
||||
const mockStreamObject = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockStreamText.mockReset();
|
||||
mockStreamObject.mockReset();
|
||||
});
|
||||
|
||||
it('should include temperature in generateText when supported', async () => {
|
||||
testProvider.supportsTemperature = true;
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
temperature: 0.7
|
||||
});
|
||||
|
||||
expect(mockGenerateText).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ temperature: 0.7 })
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude temperature in generateText when not supported', async () => {
|
||||
testProvider.supportsTemperature = false;
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
temperature: 0.7
|
||||
});
|
||||
|
||||
const callArgs = mockGenerateText.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty('temperature');
|
||||
});
|
||||
|
||||
it('should exclude temperature when undefined even if supported', async () => {
|
||||
testProvider.supportsTemperature = true;
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'response',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
await testProvider.generateText({
|
||||
apiKey: 'key',
|
||||
modelId: 'model',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
temperature: undefined
|
||||
});
|
||||
|
||||
const callArgs = mockGenerateText.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty('temperature');
|
||||
});
|
||||
});
|
||||
});
|
||||
92
tests/unit/ai-providers/codex-cli.test.js
Normal file
92
tests/unit/ai-providers/codex-cli.test.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock the ai module
|
||||
jest.unstable_mockModule('ai', () => ({
|
||||
generateObject: jest.fn(),
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock the codex-cli SDK module
|
||||
jest.unstable_mockModule('ai-sdk-provider-codex-cli', () => ({
|
||||
createCodexCli: jest.fn((options) => {
|
||||
const provider = (modelId, settings) => ({ id: modelId, settings });
|
||||
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
|
||||
provider.chat = provider.languageModel;
|
||||
return provider;
|
||||
})
|
||||
}));
|
||||
|
||||
// Mock config getters
|
||||
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
|
||||
getCodexCliSettingsForCommand: jest.fn(() => ({ allowNpx: true })),
|
||||
// Provide commonly imported getters to satisfy other module imports if any
|
||||
getDebugFlag: jest.fn(() => false),
|
||||
getLogLevel: jest.fn(() => 'info')
|
||||
}));
|
||||
|
||||
// Mock base provider
|
||||
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
|
||||
BaseAIProvider: class {
|
||||
constructor() {
|
||||
this.name = 'Base Provider';
|
||||
}
|
||||
handleError(_ctx, err) {
|
||||
throw err;
|
||||
}
|
||||
validateParams(params) {
|
||||
if (!params.modelId) throw new Error('Model ID is required');
|
||||
}
|
||||
validateMessages(msgs) {
|
||||
if (!Array.isArray(msgs)) throw new Error('Invalid messages array');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const { CodexCliProvider } = await import(
|
||||
'../../../src/ai-providers/codex-cli.js'
|
||||
);
|
||||
const { createCodexCli } = await import('ai-sdk-provider-codex-cli');
|
||||
const { getCodexCliSettingsForCommand } = await import(
|
||||
'../../../scripts/modules/config-manager.js'
|
||||
);
|
||||
|
||||
describe('CodexCliProvider', () => {
|
||||
let provider;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
provider = new CodexCliProvider();
|
||||
});
|
||||
|
||||
it('sets provider name and supported models', () => {
|
||||
expect(provider.name).toBe('Codex CLI');
|
||||
expect(provider.supportedModels).toEqual(['gpt-5', 'gpt-5-codex']);
|
||||
});
|
||||
|
||||
it('does not require API key', () => {
|
||||
expect(provider.isRequiredApiKey()).toBe(false);
|
||||
});
|
||||
|
||||
it('creates client with merged default settings', async () => {
|
||||
const client = await provider.getClient({ commandName: 'parse-prd' });
|
||||
expect(client).toBeDefined();
|
||||
expect(createCodexCli).toHaveBeenCalledWith({
|
||||
defaultSettings: expect.objectContaining({ allowNpx: true })
|
||||
});
|
||||
expect(getCodexCliSettingsForCommand).toHaveBeenCalledWith('parse-prd');
|
||||
});
|
||||
|
||||
it('injects OPENAI_API_KEY only when apiKey provided', async () => {
|
||||
const client = await provider.getClient({
|
||||
commandName: 'expand',
|
||||
apiKey: 'sk-test'
|
||||
});
|
||||
const call = createCodexCli.mock.calls[0][0];
|
||||
expect(call.defaultSettings.env.OPENAI_API_KEY).toBe('sk-test');
|
||||
// Ensure env is not set when apiKey not provided
|
||||
await provider.getClient({ commandName: 'expand' });
|
||||
const second = createCodexCli.mock.calls[1][0];
|
||||
expect(second.defaultSettings.env).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -122,7 +122,7 @@ jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
||||
getMcpApiKeyStatus: mockGetMcpApiKeyStatus,
|
||||
|
||||
// Providers without API keys
|
||||
providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli']
|
||||
providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli', 'codex-cli']
|
||||
}));
|
||||
|
||||
// Mock AI Provider Classes with proper methods
|
||||
@@ -158,6 +158,24 @@ const mockOllamaProvider = {
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
};
|
||||
|
||||
// Codex CLI mock provider instance
|
||||
const mockCodexProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'OPENAI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
};
|
||||
|
||||
// Claude Code mock provider instance
|
||||
const mockClaudeProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
};
|
||||
|
||||
// Mock the provider classes to return our mock instances
|
||||
jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||
AnthropicAIProvider: jest.fn(() => mockAnthropicProvider),
|
||||
@@ -213,13 +231,7 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||
getRequiredApiKeyName: jest.fn(() => null),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
})),
|
||||
ClaudeCodeProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
})),
|
||||
ClaudeCodeProvider: jest.fn(() => mockClaudeProvider),
|
||||
GeminiCliProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
@@ -227,6 +239,7 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||
getRequiredApiKeyName: jest.fn(() => 'GEMINI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
})),
|
||||
CodexCliProvider: jest.fn(() => mockCodexProvider),
|
||||
GrokCliProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
@@ -809,5 +822,112 @@ describe('Unified AI Services', () => {
|
||||
// Should have gotten the anthropic response
|
||||
expect(result.mainResult).toBe('Anthropic response with session key');
|
||||
});
|
||||
|
||||
// --- Codex CLI specific tests ---
|
||||
test('should use codex-cli provider without API key (OAuth)', async () => {
|
||||
// Arrange codex-cli as main provider
|
||||
mockGetMainProvider.mockReturnValue('codex-cli');
|
||||
mockGetMainModelId.mockReturnValue('gpt-5-codex');
|
||||
mockGetParametersForRole.mockReturnValue({
|
||||
maxTokens: 128000,
|
||||
temperature: 1
|
||||
});
|
||||
mockGetResponseLanguage.mockReturnValue('English');
|
||||
// No API key in env
|
||||
mockResolveEnvVariable.mockReturnValue(null);
|
||||
// Mock codex generateText response
|
||||
mockCodexProvider.generateText.mockResolvedValueOnce({
|
||||
text: 'ok',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
const { generateTextService } = await import(
|
||||
'../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
const result = await generateTextService({
|
||||
role: 'main',
|
||||
prompt: 'Hello Codex',
|
||||
projectRoot: fakeProjectRoot
|
||||
});
|
||||
|
||||
expect(result.mainResult).toBe('ok');
|
||||
expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelId: 'gpt-5-codex',
|
||||
apiKey: null,
|
||||
maxTokens: 128000
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should pass apiKey to codex-cli when provided', async () => {
|
||||
// Arrange codex-cli as main provider
|
||||
mockGetMainProvider.mockReturnValue('codex-cli');
|
||||
mockGetMainModelId.mockReturnValue('gpt-5-codex');
|
||||
mockGetParametersForRole.mockReturnValue({
|
||||
maxTokens: 128000,
|
||||
temperature: 1
|
||||
});
|
||||
mockGetResponseLanguage.mockReturnValue('English');
|
||||
// Provide API key via env resolver
|
||||
mockResolveEnvVariable.mockReturnValue('sk-test');
|
||||
// Mock codex generateText response
|
||||
mockCodexProvider.generateText.mockResolvedValueOnce({
|
||||
text: 'ok-with-key',
|
||||
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
||||
});
|
||||
|
||||
const { generateTextService } = await import(
|
||||
'../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
const result = await generateTextService({
|
||||
role: 'main',
|
||||
prompt: 'Hello Codex',
|
||||
projectRoot: fakeProjectRoot
|
||||
});
|
||||
|
||||
expect(result.mainResult).toBe('ok-with-key');
|
||||
expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelId: 'gpt-5-codex',
|
||||
apiKey: 'sk-test'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// --- Claude Code specific test ---
|
||||
test('should pass temperature to claude-code provider (provider handles filtering)', async () => {
|
||||
mockGetMainProvider.mockReturnValue('claude-code');
|
||||
mockGetMainModelId.mockReturnValue('sonnet');
|
||||
mockGetParametersForRole.mockReturnValue({
|
||||
maxTokens: 64000,
|
||||
temperature: 0.7
|
||||
});
|
||||
mockGetResponseLanguage.mockReturnValue('English');
|
||||
mockResolveEnvVariable.mockReturnValue(null);
|
||||
|
||||
mockClaudeProvider.generateText.mockResolvedValueOnce({
|
||||
text: 'ok-claude',
|
||||
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
|
||||
});
|
||||
|
||||
const { generateTextService } = await import(
|
||||
'../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
const result = await generateTextService({
|
||||
role: 'main',
|
||||
prompt: 'Hello Claude',
|
||||
projectRoot: fakeProjectRoot
|
||||
});
|
||||
|
||||
expect(result.mainResult).toBe('ok-claude');
|
||||
// The provider (BaseAIProvider) is responsible for filtering it based on supportsTemperature
|
||||
const callArgs = mockClaudeProvider.generateText.mock.calls[0][0];
|
||||
expect(callArgs).toHaveProperty('temperature', 0.7);
|
||||
expect(callArgs.maxTokens).toBe(64000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,6 +149,7 @@ const DEFAULT_CONFIG = {
|
||||
responseLanguage: 'English'
|
||||
},
|
||||
claudeCode: {},
|
||||
codexCli: {},
|
||||
grokCli: {
|
||||
timeout: 120000,
|
||||
workingDirectory: null,
|
||||
@@ -642,7 +643,8 @@ describe('getConfig Tests', () => {
|
||||
...DEFAULT_CONFIG.claudeCode,
|
||||
...VALID_CUSTOM_CONFIG.claudeCode
|
||||
},
|
||||
grokCli: { ...DEFAULT_CONFIG.grokCli }
|
||||
grokCli: { ...DEFAULT_CONFIG.grokCli },
|
||||
codexCli: { ...DEFAULT_CONFIG.codexCli }
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||
@@ -685,7 +687,8 @@ describe('getConfig Tests', () => {
|
||||
...DEFAULT_CONFIG.claudeCode,
|
||||
...VALID_CUSTOM_CONFIG.claudeCode
|
||||
},
|
||||
grokCli: { ...DEFAULT_CONFIG.grokCli }
|
||||
grokCli: { ...DEFAULT_CONFIG.grokCli },
|
||||
codexCli: { ...DEFAULT_CONFIG.codexCli }
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
|
||||
@@ -794,7 +797,8 @@ describe('getConfig Tests', () => {
|
||||
...DEFAULT_CONFIG.claudeCode,
|
||||
...VALID_CUSTOM_CONFIG.claudeCode
|
||||
},
|
||||
grokCli: { ...DEFAULT_CONFIG.grokCli }
|
||||
grokCli: { ...DEFAULT_CONFIG.grokCli },
|
||||
codexCli: { ...DEFAULT_CONFIG.codexCli }
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { PromptManager } from '../../../scripts/modules/prompt-manager.js';
|
||||
import { ExpandTaskResponseSchema } from '../../../src/schemas/expand-task.js';
|
||||
import { SubtaskSchema } from '../../../src/schemas/base-schemas.js';
|
||||
|
||||
describe('expand-task prompt template', () => {
|
||||
let promptManager;
|
||||
@@ -74,30 +75,25 @@ describe('expand-task prompt template', () => {
|
||||
expect(userPrompt).toContain(`Current details: ${testTask.details}`);
|
||||
|
||||
// Also includes the expansion prompt
|
||||
expect(userPrompt).toContain('Expansion Guidance:');
|
||||
expect(userPrompt).toContain(params.expansionPrompt);
|
||||
expect(userPrompt).toContain(params.complexityReasoningContext);
|
||||
});
|
||||
|
||||
test('all variants request JSON format with subtasks array', () => {
|
||||
const variants = ['default', 'research', 'complexity-report'];
|
||||
test('ExpandTaskResponseSchema defines required subtask fields', () => {
|
||||
// Test the schema definition directly instead of weak substring matching
|
||||
const schema = ExpandTaskResponseSchema;
|
||||
const subtasksSchema = schema.shape.subtasks;
|
||||
const subtaskSchema = subtasksSchema.element;
|
||||
|
||||
variants.forEach((variant) => {
|
||||
const params =
|
||||
variant === 'complexity-report'
|
||||
? { ...baseParams, expansionPrompt: 'test' }
|
||||
: baseParams;
|
||||
|
||||
const { systemPrompt, userPrompt } = promptManager.loadPrompt(
|
||||
'expand-task',
|
||||
params,
|
||||
variant
|
||||
);
|
||||
const combined = systemPrompt + userPrompt;
|
||||
|
||||
expect(combined.toLowerCase()).toContain('subtasks');
|
||||
expect(combined).toContain('JSON');
|
||||
});
|
||||
// Verify the schema has the required fields
|
||||
expect(subtaskSchema).toBe(SubtaskSchema);
|
||||
expect(SubtaskSchema.shape).toHaveProperty('id');
|
||||
expect(SubtaskSchema.shape).toHaveProperty('title');
|
||||
expect(SubtaskSchema.shape).toHaveProperty('description');
|
||||
expect(SubtaskSchema.shape).toHaveProperty('dependencies');
|
||||
expect(SubtaskSchema.shape).toHaveProperty('details');
|
||||
expect(SubtaskSchema.shape).toHaveProperty('status');
|
||||
expect(SubtaskSchema.shape).toHaveProperty('testStrategy');
|
||||
});
|
||||
|
||||
test('complexity-report variant fails without task context regression test', () => {
|
||||
|
||||
55
tests/unit/prompts/prompt-migration.test.js
Normal file
55
tests/unit/prompts/prompt-migration.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const promptsDir = path.join(__dirname, '../../../src/prompts');
|
||||
|
||||
describe('Prompt Migration Validation', () => {
|
||||
const bannedPhrases = [
|
||||
'Respond ONLY with',
|
||||
'Return only the',
|
||||
'valid JSON',
|
||||
'Do not include any explanatory text',
|
||||
'Do not include any explanation',
|
||||
'code block markers'
|
||||
];
|
||||
|
||||
// Map banned phrases to contexts where they're allowed
|
||||
const allowedContexts = {
|
||||
'respond only with': ['Use markdown formatting for better readability'],
|
||||
'return only the': ['Use markdown formatting for better readability']
|
||||
};
|
||||
|
||||
test('prompts should not contain JSON formatting instructions', () => {
|
||||
const promptFiles = fs
|
||||
.readdirSync(promptsDir)
|
||||
.filter((file) => file.endsWith('.json') && !file.includes('schema'))
|
||||
// Exclude update-subtask.json as it returns plain strings, not JSON
|
||||
.filter((file) => file !== 'update-subtask.json');
|
||||
|
||||
promptFiles.forEach((file) => {
|
||||
const content = fs.readFileSync(path.join(promptsDir, file), 'utf8');
|
||||
|
||||
bannedPhrases.forEach((phrase) => {
|
||||
const lowerContent = content.toLowerCase();
|
||||
const lowerPhrase = phrase.toLowerCase();
|
||||
|
||||
if (lowerContent.includes(lowerPhrase)) {
|
||||
// Check if this phrase is allowed in its context
|
||||
const allowedInContext = allowedContexts[lowerPhrase];
|
||||
const isAllowed =
|
||||
allowedInContext &&
|
||||
allowedInContext.some((context) =>
|
||||
lowerContent.includes(context.toLowerCase())
|
||||
);
|
||||
|
||||
expect(isAllowed).toBe(
|
||||
true,
|
||||
`File ${file} contains banned phrase "${phrase}" without allowed context`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -94,7 +94,6 @@ describe('addSubtask function', () => {
|
||||
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
|
||||
expect(parentTask.subtasks).toHaveLength(1);
|
||||
expect(parentTask.subtasks[0].title).toBe('New Subtask');
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should convert an existing task to a subtask', async () => {
|
||||
|
||||
@@ -50,7 +50,7 @@ jest.unstable_mockModule(
|
||||
() => ({
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
tasks: []
|
||||
complexityAnalysis: []
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -307,10 +307,15 @@ describe('analyzeTaskComplexity', () => {
|
||||
return { task: task || null, originalSubtaskCount: null };
|
||||
});
|
||||
|
||||
generateTextService.mockResolvedValue(sampleApiResponse);
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
complexityAnalysis: JSON.parse(sampleApiResponse.mainResult).tasks
|
||||
},
|
||||
telemetryData: sampleApiResponse.telemetryData
|
||||
});
|
||||
});
|
||||
|
||||
test('should call generateTextService with the correct parameters', async () => {
|
||||
test('should call generateObjectService with the correct parameters', async () => {
|
||||
// Arrange
|
||||
const options = {
|
||||
file: 'tasks/tasks.json',
|
||||
@@ -338,7 +343,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
'/mock/project/root',
|
||||
undefined
|
||||
);
|
||||
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('task-complexity-report.json'),
|
||||
expect.stringContaining('"thresholdScore": 5'),
|
||||
@@ -369,7 +374,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
role: 'research' // This should be present when research is true
|
||||
})
|
||||
@@ -454,7 +459,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
|
||||
// Assert
|
||||
// Check if the prompt sent to AI doesn't include the completed task (id: 3)
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.not.stringContaining('"id": 3')
|
||||
})
|
||||
@@ -471,7 +476,7 @@ describe('analyzeTaskComplexity', () => {
|
||||
};
|
||||
|
||||
// Force API error
|
||||
generateTextService.mockRejectedValueOnce(new Error('API Error'));
|
||||
generateObjectService.mockRejectedValueOnce(new Error('API Error'));
|
||||
|
||||
const mockMcpLog = {
|
||||
info: jest.fn(),
|
||||
|
||||
@@ -196,9 +196,62 @@ jest.unstable_mockModule(
|
||||
currency: 'USD'
|
||||
}
|
||||
}),
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
object: {
|
||||
generateObjectService: jest.fn().mockImplementation((params) => {
|
||||
const commandName = params?.commandName || 'default';
|
||||
|
||||
if (commandName === 'analyze-complexity') {
|
||||
// Check if this is for a specific tag test by looking at the prompt
|
||||
const isFeatureTag =
|
||||
params?.prompt?.includes('feature') || params?.role === 'feature';
|
||||
const isMasterTag =
|
||||
params?.prompt?.includes('master') || params?.role === 'master';
|
||||
|
||||
let taskTitle = 'Test Task';
|
||||
if (isFeatureTag) {
|
||||
taskTitle = 'Feature Task 1';
|
||||
} else if (isMasterTag) {
|
||||
taskTitle = 'Master Task 1';
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
mainResult: {
|
||||
complexityAnalysis: [
|
||||
{
|
||||
taskId: 1,
|
||||
taskTitle: taskTitle,
|
||||
complexityScore: 7,
|
||||
recommendedSubtasks: 4,
|
||||
expansionPrompt: 'Break down this task',
|
||||
reasoning: 'This task is moderately complex'
|
||||
},
|
||||
{
|
||||
taskId: 2,
|
||||
taskTitle: 'Task 2',
|
||||
complexityScore: 5,
|
||||
recommendedSubtasks: 3,
|
||||
expansionPrompt: 'Break down this task with a focus on task 2.',
|
||||
reasoning:
|
||||
'Automatically added due to missing analysis in AI response.'
|
||||
}
|
||||
]
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
commandName: 'analyze-complexity',
|
||||
modelUsed: 'claude-3-5-sonnet',
|
||||
providerName: 'anthropic',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Default response for expand-task and others
|
||||
return Promise.resolve({
|
||||
mainResult: {
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
@@ -210,19 +263,19 @@ jest.unstable_mockModule(
|
||||
testStrategy: 'Test strategy'
|
||||
}
|
||||
]
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
commandName: 'expand-task',
|
||||
modelUsed: 'claude-3-5-sonnet',
|
||||
providerName: 'anthropic',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
commandName: 'expand-task',
|
||||
modelUsed: 'claude-3-5-sonnet',
|
||||
providerName: 'anthropic',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
totalCost: 0.012414,
|
||||
currency: 'USD'
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -421,9 +474,8 @@ const { readJSON, writeJSON, getTagAwareFilePath } = await import(
|
||||
'../../../../../scripts/modules/utils.js'
|
||||
);
|
||||
|
||||
const { generateTextService, streamTextService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
const { generateTextService, generateObjectService, streamTextService } =
|
||||
await import('../../../../../scripts/modules/ai-services-unified.js');
|
||||
|
||||
// Import the modules under test
|
||||
const { default: analyzeTaskComplexity } = await import(
|
||||
|
||||
@@ -65,8 +65,8 @@ jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
|
||||
jest.unstable_mockModule(
|
||||
'../../../../../scripts/modules/ai-services-unified.js',
|
||||
() => ({
|
||||
generateTextService: jest.fn().mockResolvedValue({
|
||||
mainResult: JSON.stringify({
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
@@ -101,7 +101,7 @@ jest.unstable_mockModule(
|
||||
testStrategy: 'UI tests and visual regression testing'
|
||||
}
|
||||
]
|
||||
}),
|
||||
},
|
||||
telemetryData: {
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: '1234567890',
|
||||
@@ -213,7 +213,7 @@ const {
|
||||
findProjectRoot
|
||||
} = await import('../../../../../scripts/modules/utils.js');
|
||||
|
||||
const { generateTextService } = await import(
|
||||
const { generateObjectService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
@@ -373,7 +373,7 @@ describe('expandTask', () => {
|
||||
'/mock/project/root',
|
||||
undefined
|
||||
);
|
||||
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(writeJSON).toHaveBeenCalledWith(
|
||||
tasksPath,
|
||||
expect.objectContaining({
|
||||
@@ -458,7 +458,7 @@ describe('expandTask', () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
role: 'research',
|
||||
commandName: expect.any(String)
|
||||
@@ -496,7 +496,7 @@ describe('expandTask', () => {
|
||||
telemetryData: expect.any(Object)
|
||||
})
|
||||
);
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -660,25 +660,38 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, 3, false, '', context, false);
|
||||
|
||||
// Assert - Should append to existing subtasks with proper ID increments
|
||||
expect(writeJSON).toHaveBeenCalledWith(
|
||||
tasksPath,
|
||||
// Assert - Verify generateObjectService was called correctly
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tasks: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 4,
|
||||
subtasks: expect.arrayContaining([
|
||||
// Should contain both existing and new subtasks
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object) // 1 existing + 3 new = 4 total
|
||||
])
|
||||
})
|
||||
])
|
||||
}),
|
||||
'/mock/project/root',
|
||||
undefined
|
||||
role: 'main',
|
||||
commandName: 'expand-task',
|
||||
objectName: 'subtasks'
|
||||
})
|
||||
);
|
||||
|
||||
// Assert - Verify data was written with appended subtasks
|
||||
expect(writeJSON).toHaveBeenCalled();
|
||||
const writeCall = writeJSON.mock.calls[0];
|
||||
const savedData = writeCall[1]; // Second argument is the data
|
||||
const task4 = savedData.tasks.find((t) => t.id === 4);
|
||||
|
||||
// Should have 4 subtasks total (1 existing + 3 new)
|
||||
expect(task4.subtasks).toHaveLength(4);
|
||||
|
||||
// Verify existing subtask is preserved at index 0
|
||||
expect(task4.subtasks[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 1,
|
||||
title: 'Existing subtask'
|
||||
})
|
||||
);
|
||||
|
||||
// Verify new subtasks were appended (they start with id=1 from AI)
|
||||
expect(task4.subtasks[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 1,
|
||||
title: 'Set up project structure'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -743,8 +756,8 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
|
||||
|
||||
// Assert - generateTextService called with systemPrompt for 5 subtasks
|
||||
const callArg = generateTextService.mock.calls[0][0];
|
||||
// Assert - generateObjectService called with systemPrompt for 5 subtasks
|
||||
const callArg = generateObjectService.mock.calls[0][0];
|
||||
expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
|
||||
|
||||
// Assert - Should use complexity-report variant with expansion prompt
|
||||
@@ -831,7 +844,9 @@ describe('expandTask', () => {
|
||||
projectRoot: '/mock/project/root'
|
||||
};
|
||||
|
||||
generateTextService.mockRejectedValueOnce(new Error('AI service error'));
|
||||
generateObjectService.mockRejectedValueOnce(
|
||||
new Error('AI service error')
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
@@ -841,6 +856,54 @@ describe('expandTask', () => {
|
||||
expect(writeJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle missing mainResult from AI response', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const taskId = '2';
|
||||
const context = {
|
||||
mcpLog: createMcpLogMock(),
|
||||
projectRoot: '/mock/project/root'
|
||||
};
|
||||
|
||||
// Mock AI service returning response without mainResult
|
||||
generateObjectService.mockResolvedValueOnce({
|
||||
telemetryData: { inputTokens: 100, outputTokens: 50 }
|
||||
// Missing mainResult
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
expandTask(tasksPath, taskId, 3, false, '', context, false)
|
||||
).rejects.toThrow('AI response did not include a valid subtasks array.');
|
||||
|
||||
expect(writeJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle invalid subtasks array from AI response', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const taskId = '2';
|
||||
const context = {
|
||||
mcpLog: createMcpLogMock(),
|
||||
projectRoot: '/mock/project/root'
|
||||
};
|
||||
|
||||
// Mock AI service returning response with invalid subtasks
|
||||
generateObjectService.mockResolvedValueOnce({
|
||||
mainResult: {
|
||||
subtasks: 'not-an-array' // Invalid: should be an array
|
||||
},
|
||||
telemetryData: { inputTokens: 100, outputTokens: 50 }
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
expandTask(tasksPath, taskId, 3, false, '', context, false)
|
||||
).rejects.toThrow('AI response did not include a valid subtasks array.');
|
||||
|
||||
expect(writeJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle file read errors', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
@@ -941,7 +1004,7 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, 3, false, '', context, false);
|
||||
|
||||
// Assert - Should work with empty context (but may include project context)
|
||||
expect(generateTextService).toHaveBeenCalledWith(
|
||||
expect(generateObjectService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
|
||||
})
|
||||
@@ -1074,7 +1137,7 @@ describe('expandTask', () => {
|
||||
|
||||
// Assert - Should complete successfully
|
||||
expect(result).toBeDefined();
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use dynamic prompting when numSubtasks is 0', async () => {
|
||||
@@ -1095,11 +1158,11 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, 0, false, '', context, false);
|
||||
|
||||
// Assert - Verify generateTextService was called
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
// Assert - Verify generateObjectService was called
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
|
||||
// Get the call arguments to verify the system prompt
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain(
|
||||
'an appropriate number of specific subtasks'
|
||||
);
|
||||
@@ -1122,11 +1185,11 @@ describe('expandTask', () => {
|
||||
// Act
|
||||
await expandTask(tasksPath, taskId, 5, false, '', context, false);
|
||||
|
||||
// Assert - Verify generateTextService was called
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
// Assert - Verify generateObjectService was called
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
|
||||
// Get the call arguments to verify the system prompt
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('5 specific subtasks');
|
||||
});
|
||||
|
||||
@@ -1151,8 +1214,8 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, -3, false, '', context, false);
|
||||
|
||||
// Assert - Should use default value instead of negative
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('4 specific subtasks');
|
||||
});
|
||||
|
||||
@@ -1177,8 +1240,8 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
|
||||
|
||||
// Assert - Should use default value
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('6 specific subtasks');
|
||||
});
|
||||
|
||||
@@ -1203,8 +1266,8 @@ describe('expandTask', () => {
|
||||
await expandTask(tasksPath, taskId, null, false, '', context, false);
|
||||
|
||||
// Assert - Should use default value
|
||||
expect(generateTextService).toHaveBeenCalled();
|
||||
const callArgs = generateTextService.mock.calls[0][0];
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
expect(callArgs.systemPrompt).toContain('7 specific subtasks');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,11 +88,6 @@ describe('moveTask (unit)', () => {
|
||||
).rejects.toThrow(/Number of source IDs/);
|
||||
});
|
||||
|
||||
test('batch move calls generateTaskFiles once when flag true', async () => {
|
||||
await moveTask('tasks.json', '1,2', '3,4', true, { tag: 'master' });
|
||||
expect(generateTaskFiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('error when tag invalid', async () => {
|
||||
await expect(
|
||||
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })
|
||||
|
||||
@@ -43,7 +43,23 @@ jest.unstable_mockModule(
|
||||
() => ({
|
||||
generateTextService: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} })
|
||||
.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} }),
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
task: {
|
||||
id: 1,
|
||||
title: 'Updated Task',
|
||||
description: 'Updated description',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'medium',
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
},
|
||||
telemetryData: {}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
@@ -120,3 +136,206 @@ describe('updateTaskById validation', () => {
|
||||
expect(log).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTaskById success path with generateObjectService', () => {
|
||||
let fs;
|
||||
let generateObjectService;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
fs = await import('fs');
|
||||
const aiServices = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
generateObjectService = aiServices.generateObjectService;
|
||||
});
|
||||
|
||||
test('successfully updates task with all fields from generateObjectService', async () => {
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
readJSON.mockReturnValue({
|
||||
tag: 'master',
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Original Task',
|
||||
description: 'Original description',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'low',
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const updatedTaskData = {
|
||||
id: 1,
|
||||
title: 'Updated Task',
|
||||
description: 'Updated description',
|
||||
status: 'pending',
|
||||
dependencies: [2],
|
||||
priority: 'high',
|
||||
details: 'New implementation details',
|
||||
testStrategy: 'Unit tests required',
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Subtask 1',
|
||||
description: 'First subtask',
|
||||
status: 'pending',
|
||||
dependencies: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
task: updatedTaskData
|
||||
},
|
||||
telemetryData: {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
inputTokens: 100,
|
||||
outputTokens: 200
|
||||
}
|
||||
});
|
||||
|
||||
const result = await updateTaskById(
|
||||
'tasks/tasks.json',
|
||||
1,
|
||||
'Update task with new requirements',
|
||||
false,
|
||||
{ tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
|
||||
// Verify generateObjectService was called (not generateTextService)
|
||||
expect(generateObjectService).toHaveBeenCalled();
|
||||
const callArgs = generateObjectService.mock.calls[0][0];
|
||||
|
||||
// Verify correct arguments were passed
|
||||
expect(callArgs).toMatchObject({
|
||||
role: 'main',
|
||||
commandName: 'update-task',
|
||||
objectName: 'task'
|
||||
});
|
||||
expect(callArgs.schema).toBeDefined();
|
||||
expect(callArgs.systemPrompt).toContain(
|
||||
'update a software development task'
|
||||
);
|
||||
expect(callArgs.prompt).toContain('Update task with new requirements');
|
||||
|
||||
// Verify the returned task contains all expected fields
|
||||
expect(result).toEqual({
|
||||
updatedTask: expect.objectContaining({
|
||||
id: 1,
|
||||
title: 'Updated Task',
|
||||
description: 'Updated description',
|
||||
status: 'pending',
|
||||
dependencies: [2],
|
||||
priority: 'high',
|
||||
details: 'New implementation details',
|
||||
testStrategy: 'Unit tests required',
|
||||
subtasks: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 1,
|
||||
title: 'Subtask 1',
|
||||
description: 'First subtask',
|
||||
status: 'pending'
|
||||
})
|
||||
])
|
||||
}),
|
||||
telemetryData: expect.objectContaining({
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
inputTokens: 100,
|
||||
outputTokens: 200
|
||||
}),
|
||||
tagInfo: undefined
|
||||
});
|
||||
});
|
||||
|
||||
test('handles generateObjectService with malformed mainResult', async () => {
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
readJSON.mockReturnValue({
|
||||
tag: 'master',
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task',
|
||||
description: 'Description',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'medium',
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
task: null // Malformed: task is null
|
||||
},
|
||||
telemetryData: {}
|
||||
});
|
||||
|
||||
await expect(
|
||||
updateTaskById(
|
||||
'tasks/tasks.json',
|
||||
1,
|
||||
'Update task',
|
||||
false,
|
||||
{ tag: 'master' },
|
||||
'json'
|
||||
)
|
||||
).rejects.toThrow('Received invalid task object from AI');
|
||||
});
|
||||
|
||||
test('handles generateObjectService with missing required fields', async () => {
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
readJSON.mockReturnValue({
|
||||
tag: 'master',
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task',
|
||||
description: 'Description',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'medium',
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
task: {
|
||||
id: 1,
|
||||
// Missing title and description
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'medium'
|
||||
}
|
||||
},
|
||||
telemetryData: {}
|
||||
});
|
||||
|
||||
await expect(
|
||||
updateTaskById(
|
||||
'tasks/tasks.json',
|
||||
1,
|
||||
'Update task',
|
||||
false,
|
||||
{ tag: 'master' },
|
||||
'json'
|
||||
)
|
||||
).rejects.toThrow('Updated task missing required fields');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,12 @@ jest.unstable_mockModule(
|
||||
generateTextService: jest.fn().mockResolvedValue({
|
||||
mainResult: '[]', // mainResult is the text string directly
|
||||
telemetryData: {}
|
||||
}),
|
||||
generateObjectService: jest.fn().mockResolvedValue({
|
||||
mainResult: {
|
||||
tasks: [] // generateObject returns structured data
|
||||
},
|
||||
telemetryData: {}
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -84,7 +90,7 @@ const { readJSON, writeJSON, log } = await import(
|
||||
'../../../../../scripts/modules/utils.js'
|
||||
);
|
||||
|
||||
const { generateTextService } = await import(
|
||||
const { generateObjectService } = await import(
|
||||
'../../../../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
@@ -154,7 +160,9 @@ describe('updateTasks', () => {
|
||||
];
|
||||
|
||||
const mockApiResponse = {
|
||||
mainResult: JSON.stringify(mockUpdatedTasks), // mainResult is the JSON string directly
|
||||
mainResult: {
|
||||
tasks: mockUpdatedTasks // generateObject returns structured data
|
||||
},
|
||||
telemetryData: {}
|
||||
};
|
||||
|
||||
@@ -164,7 +172,7 @@ describe('updateTasks', () => {
|
||||
tag: 'master',
|
||||
_rawTaggedData: mockInitialTasks
|
||||
});
|
||||
generateTextService.mockResolvedValue(mockApiResponse);
|
||||
generateObjectService.mockResolvedValue(mockApiResponse);
|
||||
|
||||
// Act
|
||||
const result = await updateTasks(
|
||||
@@ -185,7 +193,7 @@ describe('updateTasks', () => {
|
||||
);
|
||||
|
||||
// 2. AI Service called with correct args
|
||||
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
|
||||
|
||||
// 3. Write JSON called with correctly merged tasks
|
||||
expect(writeJSON).toHaveBeenCalledWith(
|
||||
@@ -252,7 +260,7 @@ describe('updateTasks', () => {
|
||||
'/mock/path',
|
||||
'master'
|
||||
);
|
||||
expect(generateTextService).not.toHaveBeenCalled();
|
||||
expect(generateObjectService).not.toHaveBeenCalled();
|
||||
expect(writeJSON).not.toHaveBeenCalled();
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
'info',
|
||||
@@ -327,8 +335,10 @@ describe('updateTasks', () => {
|
||||
_rawTaggedData: mockTaggedData
|
||||
});
|
||||
|
||||
generateTextService.mockResolvedValue({
|
||||
mainResult: JSON.stringify(mockUpdatedTasks),
|
||||
generateObjectService.mockResolvedValue({
|
||||
mainResult: {
|
||||
tasks: mockUpdatedTasks
|
||||
},
|
||||
telemetryData: { commandName: 'update-tasks', totalCost: 0.05 }
|
||||
});
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ const getBuildTimeEnvs = () => {
|
||||
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith('TM_PUBLIC_')) {
|
||||
// Return the actual value, not JSON.stringify'd
|
||||
envs[key] = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
return envs;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"],
|
||||
"outputLogs": "new-only"
|
||||
"outputLogs": "new-only",
|
||||
"env": ["NODE_ENV", "TM_PUBLIC_*"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
|
||||
Reference in New Issue
Block a user