Compare commits

..

12 Commits

Author SHA1 Message Date
Ralph Khreish
58cc143aef remove "aiServiceResponse" duplicate
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 16:18:47 +02:00
Ralph Khreish
3aac7ac349 chore: remove unused responseText variable
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 15:44:55 +02:00
Ralph Khreish
f68330efb3 chore: apply requested changes 2025-10-02 15:30:33 +02:00
Ralph Khreish
1d197fe9c2 chore: fix CI
- adapt tests to new codebase
- improve integration tests by reducing the amount of tasks (to make the tests faster)
2025-10-02 14:53:11 +02:00
Ralph Khreish
7660a29a1a chore: implement requested changes 2025-10-02 11:54:30 +02:00
Ben Vargas
0aaa105021 test: remove integration tests that require real API calls
Integration tests that make real API calls cannot run in CI without
proper API keys. These tests should either be mocked or run in a
separate test suite with appropriate infrastructure.
2025-10-02 11:54:29 +02:00
Ben Vargas
6b15788c58 chore: remove generateObject documentation files 2025-10-02 11:54:29 +02:00
Ben Vargas
a2de49dd90 chore: fix formatting issues 2025-10-02 11:54:29 +02:00
Ben Vargas
2063dc4b7d fix: apply formatting to modified files 2025-10-02 11:54:29 +02:00
Ben Vargas
7e6319a56f chore: add changeset for generateObject migration 2025-10-02 11:54:29 +02:00
Ben Vargas
b0504a00d5 fix: enforce sequential subtask ID numbering in AI prompts
Fixed issue where AI was generating inconsistent subtask IDs (101-105, 601-603)
instead of sequential numbering (1, 2, 3...) after the generateObject migration.

Changes:
- Updated all expand-task prompt variants with forceful "CRITICAL" instructions
- Made ID requirements explicit with examples: id={{nextSubtaskId}}, id={{nextSubtaskId}}+1
- Added warning not to use parent task ID in subtask numbering
- Removed parseSubtasksFromText post-processing that was overwriting AI-generated IDs

This ensures subtasks display correctly as X.1, X.2, X.3 format and the
`tm show X.Y` command works as expected.
2025-10-02 11:54:29 +02:00
Ben Vargas
b16023ab2f feat: Complete generateObject migration with JSON mode support 2025-10-02 11:54:29 +02:00
11 changed files with 6528 additions and 243 deletions

View File

@@ -1,12 +1,5 @@
# task-master-ai
## 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

View File

@@ -1,12 +1,5 @@
# Change Log
## 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

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.4",
"version": "0.25.3",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {

6573
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -135,28 +135,15 @@ export class TaskService {
}
/**
* Get a single task by ID - delegates to storage layer
* Get a single task by ID
*/
async getTask(taskId: string, tag?: string): Promise<Task | null> {
// Use provided tag or get active tag
const activeTag = tag || this.getActiveTag();
const result = await this.getTaskList({
tag,
includeSubtasks: true
});
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
);
}
return result.tasks.find((t) => t.id === taskId) || null;
}
/**

View File

@@ -105,65 +105,9 @@ 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;
}

View File

@@ -93,55 +93,31 @@ 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',
isUnknown: false
};
const DEFAULT_COST = { inputCost: 0, outputCost: 0, currency: 'USD' };
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
log(
'warn',
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
);
return { ...DEFAULT_COST, isUnknown: true };
return DEFAULT_COST;
}
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
if (!modelData) {
if (!modelData?.cost_per_1m_tokens) {
log(
'debug',
`Model "${modelId}" not found under provider "${providerName}". Assuming unknown cost.`
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero 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 };
return DEFAULT_COST;
}
const costs = modelData.cost_per_1m_tokens;
return {
inputCost: costs.input || 0,
outputCost: costs.output || 0,
currency: costs.currency || 'USD',
isUnknown: false
currency: costs.currency || 'USD'
};
}
@@ -891,8 +867,8 @@ async function logAiUsage({
const timestamp = new Date().toISOString();
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
// Destructure currency along with costs and unknown flag
const { inputCost, outputCost, currency, isUnknown } = _getCostForModel(
// Destructure currency along with costs
const { inputCost, outputCost, currency } = _getCostForModel(
providerName,
modelId
);
@@ -914,8 +890,7 @@ async function logAiUsage({
outputTokens: outputTokens || 0,
totalTokens,
totalCost,
currency, // Add currency to the telemetry data
isUnknownCost: isUnknown // Flag to indicate if pricing is unknown
currency // Add currency to the telemetry data
};
if (getDebugFlag()) {

View File

@@ -619,29 +619,9 @@ 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: colWidths,
wordWrap: true
colWidths: showMetadata ? [20, 10, 12, 15, 50] : [25, 10, 12]
});
// Add rows

View File

@@ -2310,8 +2310,7 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
outputTokens,
totalTokens,
totalCost,
commandName,
isUnknownCost
commandName
} = telemetryData;
let summary = chalk.bold.blue('AI Usage Summary:') + '\n';
@@ -2321,10 +2320,7 @@ function displayAiUsageSummary(telemetryData, outputType = 'cli') {
summary += chalk.gray(
` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n`
);
// 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}`);
summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`);
console.log(
boxen(summary, {

View File

@@ -176,19 +176,12 @@ 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,
outputTokens,
totalTokens
inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens
}
};
} catch (error) {
@@ -303,19 +296,12 @@ 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,
outputTokens,
totalTokens
inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens
}
};
} catch (error) {