Compare commits
6 Commits
task-maste
...
ralph/fix.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099e72a93d | ||
|
|
8765832a49 | ||
|
|
6b73c42dbd | ||
|
|
cd2b4c9d56 | ||
|
|
ff9b7a46d6 | ||
|
|
108301132f |
5
.changeset/clarify-force-move-docs.md
Normal file
5
.changeset/clarify-force-move-docs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
||||
@@ -6,10 +6,12 @@
|
||||
"repo": "eyaltoledano/claude-task-master"
|
||||
}
|
||||
],
|
||||
"commit": false,
|
||||
"commit": true,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"docs"
|
||||
]
|
||||
|
||||
5
.changeset/crazy-zebras-drum.md
Normal file
5
.changeset/crazy-zebras-drum.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Restore Taskmaster claude-code commands and move clear commands under /remove to avoid collision with the claude-code /clear command.
|
||||
9
.changeset/curvy-moons-dig.md
Normal file
9
.changeset/curvy-moons-dig.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Enhanced Gemini CLI provider with codebase-aware task generation
|
||||
|
||||
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
|
||||
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
|
||||
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
|
||||
16
.changeset/pre.json
Normal file
16
.changeset/pre.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mode": "exit",
|
||||
"tag": "rc",
|
||||
"initialVersions": {
|
||||
"task-master-ai": "0.25.1",
|
||||
"docs": "0.0.1",
|
||||
"extension": "0.24.1"
|
||||
},
|
||||
"changesets": [
|
||||
"clarify-force-move-docs",
|
||||
"curvy-moons-dig",
|
||||
"sour-coins-lay",
|
||||
"strong-eagles-vanish",
|
||||
"wet-candies-accept"
|
||||
]
|
||||
}
|
||||
11
.changeset/sour-coins-lay.md
Normal file
11
.changeset/sour-coins-lay.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add configurable codebase analysis feature flag with multiple configuration sources
|
||||
|
||||
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
|
||||
|
||||
Priority order: .env > MCP session env > .taskmaster/config.json.
|
||||
|
||||
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.
|
||||
12
.changeset/strong-eagles-vanish.md
Normal file
12
.changeset/strong-eagles-vanish.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
feat(move): improve cross-tag move UX and safety
|
||||
|
||||
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
|
||||
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
|
||||
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
|
||||
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
|
||||
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
|
||||
---
|
||||
14
.changeset/wet-candies-accept.md
Normal file
14
.changeset/wet-candies-accept.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
|
||||
|
||||
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
|
||||
|
||||
Commands contextualised:
|
||||
|
||||
- add-task
|
||||
- update-subtask
|
||||
- update-task
|
||||
- update
|
||||
5
.changeset/wild-ears-look.md
Normal file
5
.changeset/wild-ears-look.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"extension": minor
|
||||
---
|
||||
|
||||
Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
@@ -2,7 +2,7 @@
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "node",
|
||||
"args": ["./dist/mcp-server.js"],
|
||||
"args": ["./mcp-server/server.js"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
157
.github/scripts/parse-metrics.mjs
vendored
157
.github/scripts/parse-metrics.mjs
vendored
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
||||
|
||||
function parseMetricsTable(content, metricName) {
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
// Match a markdown table row like: | Metric Name | value | ...
|
||||
const safeName = metricName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp(`^\\|\\s*${safeName}\\s*\\|\\s*([^|]+)\\|?`);
|
||||
const match = line.match(re);
|
||||
if (match) {
|
||||
return match[1].trim() || 'N/A';
|
||||
}
|
||||
}
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
function parseCountMetric(content, metricName) {
|
||||
const result = parseMetricsTable(content, metricName);
|
||||
// Extract number from string, handling commas and spaces
|
||||
const numberMatch = result.toString().match(/[\d,]+/);
|
||||
if (numberMatch) {
|
||||
const number = parseInt(numberMatch[0].replace(/,/g, ''));
|
||||
return isNaN(number) ? 0 : number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const metrics = {
|
||||
issues_created: 0,
|
||||
issues_closed: 0,
|
||||
prs_created: 0,
|
||||
prs_merged: 0,
|
||||
issue_avg_first_response: 'N/A',
|
||||
issue_avg_time_to_close: 'N/A',
|
||||
pr_avg_first_response: 'N/A',
|
||||
pr_avg_merge_time: 'N/A'
|
||||
};
|
||||
|
||||
// Parse issue metrics
|
||||
if (existsSync('issue_metrics.md')) {
|
||||
console.log('📄 Found issue_metrics.md, parsing...');
|
||||
const issueContent = readFileSync('issue_metrics.md', 'utf8');
|
||||
|
||||
metrics.issues_created = parseCountMetric(
|
||||
issueContent,
|
||||
'Total number of items created'
|
||||
);
|
||||
metrics.issues_closed = parseCountMetric(
|
||||
issueContent,
|
||||
'Number of items closed'
|
||||
);
|
||||
metrics.issue_avg_first_response = parseMetricsTable(
|
||||
issueContent,
|
||||
'Time to first response'
|
||||
);
|
||||
metrics.issue_avg_time_to_close = parseMetricsTable(
|
||||
issueContent,
|
||||
'Time to close'
|
||||
);
|
||||
} else {
|
||||
console.warn('[parse-metrics] issue_metrics.md not found; using defaults.');
|
||||
}
|
||||
|
||||
// Parse PR created metrics
|
||||
if (existsSync('pr_created_metrics.md')) {
|
||||
console.log('📄 Found pr_created_metrics.md, parsing...');
|
||||
const prCreatedContent = readFileSync('pr_created_metrics.md', 'utf8');
|
||||
|
||||
metrics.prs_created = parseCountMetric(
|
||||
prCreatedContent,
|
||||
'Total number of items created'
|
||||
);
|
||||
metrics.pr_avg_first_response = parseMetricsTable(
|
||||
prCreatedContent,
|
||||
'Time to first response'
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'[parse-metrics] pr_created_metrics.md not found; using defaults.'
|
||||
);
|
||||
}
|
||||
|
||||
// Parse PR merged metrics (for more accurate merge data)
|
||||
if (existsSync('pr_merged_metrics.md')) {
|
||||
console.log('📄 Found pr_merged_metrics.md, parsing...');
|
||||
const prMergedContent = readFileSync('pr_merged_metrics.md', 'utf8');
|
||||
|
||||
metrics.prs_merged = parseCountMetric(
|
||||
prMergedContent,
|
||||
'Total number of items created'
|
||||
);
|
||||
// For merged PRs, "Time to close" is actually time to merge
|
||||
metrics.pr_avg_merge_time = parseMetricsTable(
|
||||
prMergedContent,
|
||||
'Time to close'
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'[parse-metrics] pr_merged_metrics.md not found; falling back to pr_metrics.md.'
|
||||
);
|
||||
// Fallback: try old pr_metrics.md if it exists
|
||||
if (existsSync('pr_metrics.md')) {
|
||||
console.log('📄 Falling back to pr_metrics.md...');
|
||||
const prContent = readFileSync('pr_metrics.md', 'utf8');
|
||||
|
||||
const mergedCount = parseCountMetric(prContent, 'Number of items merged');
|
||||
metrics.prs_merged =
|
||||
mergedCount || parseCountMetric(prContent, 'Number of items closed');
|
||||
|
||||
const maybeMergeTime = parseMetricsTable(
|
||||
prContent,
|
||||
'Average time to merge'
|
||||
);
|
||||
metrics.pr_avg_merge_time =
|
||||
maybeMergeTime !== 'N/A'
|
||||
? maybeMergeTime
|
||||
: parseMetricsTable(prContent, 'Time to close');
|
||||
} else {
|
||||
console.warn('[parse-metrics] pr_metrics.md not found; using defaults.');
|
||||
}
|
||||
}
|
||||
|
||||
// Output for GitHub Actions
|
||||
const output = Object.entries(metrics)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n');
|
||||
|
||||
// Always output to stdout for debugging
|
||||
console.log('\n=== FINAL METRICS ===');
|
||||
Object.entries(metrics).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${value}`);
|
||||
});
|
||||
|
||||
// Write to GITHUB_OUTPUT if in GitHub Actions
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
try {
|
||||
writeFileSync(process.env.GITHUB_OUTPUT, output + '\n', { flag: 'a' });
|
||||
console.log(
|
||||
`\nSuccessfully wrote metrics to ${process.env.GITHUB_OUTPUT}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to write to GITHUB_OUTPUT: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'\nNo GITHUB_OUTPUT environment variable found, skipping file write'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -11,10 +11,6 @@ on:
|
||||
- next
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -92,16 +88,6 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
test:
|
||||
name: Test
|
||||
@@ -122,11 +108,10 @@ jobs:
|
||||
run: npm install --frozen-lockfile --prefer-offline
|
||||
timeout-minutes: 5
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: dist/
|
||||
- name: Build packages (required for tests)
|
||||
run: npm run build:packages
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
|
||||
14
.github/workflows/pre-release.yml
vendored
14
.github/workflows/pre-release.yml
vendored
@@ -65,20 +65,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Run format
|
||||
run: npm run format
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
- name: Build packages
|
||||
run: npm run turbo:build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
@@ -41,15 +41,6 @@ jobs:
|
||||
- name: Check pre-release mode
|
||||
run: node ./.github/scripts/check-pre-release-mode.mjs "main"
|
||||
|
||||
- name: Build packages
|
||||
run: npm run turbo:build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FORCE_COLOR: 1
|
||||
TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }}
|
||||
TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }}
|
||||
TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
|
||||
80
.github/workflows/weekly-metrics-discord.yml
vendored
80
.github/workflows/weekly-metrics-discord.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
@@ -17,25 +17,15 @@ jobs:
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Get dates for last 14 days
|
||||
- name: Get dates for last week
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
# Last 14 days
|
||||
first_day=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
# Last 7 days
|
||||
first_day=$(date -d "7 days ago" +%Y-%m-%d)
|
||||
last_day=$(date +%Y-%m-%d)
|
||||
|
||||
echo "first_day=$first_day" >> $GITHUB_ENV
|
||||
echo "last_day=$last_day" >> $GITHUB_ENV
|
||||
echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV
|
||||
echo "date_range=Past 14 days ($first_day to $last_day)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate issue metrics
|
||||
uses: github/issue-metrics@v3
|
||||
@@ -44,39 +34,40 @@ jobs:
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
|
||||
HIDE_TIME_TO_ANSWER: true
|
||||
HIDE_LABEL_METRICS: false
|
||||
OUTPUT_FILE: issue_metrics.md
|
||||
|
||||
- name: Generate PR created metrics
|
||||
- name: Generate PR metrics
|
||||
uses: github/issue-metrics@v3
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}"
|
||||
OUTPUT_FILE: pr_created_metrics.md
|
||||
|
||||
- name: Generate PR merged metrics
|
||||
uses: github/issue-metrics@v3
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SEARCH_QUERY: "repo:${{ github.repository }} is:pr is:merged merged:${{ env.first_day }}..${{ env.last_day }}"
|
||||
OUTPUT_FILE: pr_merged_metrics.md
|
||||
|
||||
- name: Debug generated metrics
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
echo "Listing markdown files in workspace:"
|
||||
ls -la *.md || true
|
||||
for f in issue_metrics.md pr_created_metrics.md pr_merged_metrics.md; do
|
||||
if [ -f "$f" ]; then
|
||||
echo "== $f (first 10 lines) =="
|
||||
head -n 10 "$f"
|
||||
else
|
||||
echo "Missing $f"
|
||||
fi
|
||||
done
|
||||
OUTPUT_FILE: pr_metrics.md
|
||||
|
||||
- name: Parse metrics
|
||||
id: metrics
|
||||
run: node .github/scripts/parse-metrics.mjs
|
||||
run: |
|
||||
# Parse the metrics from the generated markdown files
|
||||
if [ -f "issue_metrics.md" ]; then
|
||||
# Extract key metrics using grep/awk
|
||||
AVG_TIME_TO_FIRST_RESPONSE=$(grep -A 1 "Average time to first response" issue_metrics.md | tail -1 | xargs || echo "N/A")
|
||||
AVG_TIME_TO_CLOSE=$(grep -A 1 "Average time to close" issue_metrics.md | tail -1 | xargs || echo "N/A")
|
||||
NUM_ISSUES_CREATED=$(grep -oP '\d+(?= issues created)' issue_metrics.md || echo "0")
|
||||
NUM_ISSUES_CLOSED=$(grep -oP '\d+(?= issues closed)' issue_metrics.md || echo "0")
|
||||
fi
|
||||
|
||||
if [ -f "pr_metrics.md" ]; then
|
||||
PR_AVG_TIME_TO_MERGE=$(grep -A 1 "Average time to close" pr_metrics.md | tail -1 | xargs || echo "N/A")
|
||||
NUM_PRS_CREATED=$(grep -oP '\d+(?= pull requests created)' pr_metrics.md || echo "0")
|
||||
NUM_PRS_MERGED=$(grep -oP '\d+(?= pull requests closed)' pr_metrics.md || echo "0")
|
||||
fi
|
||||
|
||||
# Set outputs for Discord action
|
||||
echo "issues_created=${NUM_ISSUES_CREATED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "issues_closed=${NUM_ISSUES_CLOSED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "prs_created=${NUM_PRS_CREATED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "prs_merged=${NUM_PRS_MERGED:-0}" >> $GITHUB_OUTPUT
|
||||
echo "avg_first_response=${AVG_TIME_TO_FIRST_RESPONSE:-N/A}" >> $GITHUB_OUTPUT
|
||||
echo "avg_time_to_close=${AVG_TIME_TO_CLOSE:-N/A}" >> $GITHUB_OUTPUT
|
||||
echo "pr_avg_merge_time=${PR_AVG_TIME_TO_MERGE:-N/A}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Send to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
@@ -87,22 +78,19 @@ jobs:
|
||||
title: "📊 Weekly Metrics Report"
|
||||
description: |
|
||||
**${{ env.week_of }}**
|
||||
*${{ env.date_range }}*
|
||||
|
||||
**🎯 Issues**
|
||||
• Created: ${{ steps.metrics.outputs.issues_created }}
|
||||
• Closed: ${{ steps.metrics.outputs.issues_closed }}
|
||||
• Avg Response Time: ${{ steps.metrics.outputs.issue_avg_first_response }}
|
||||
• Avg Time to Close: ${{ steps.metrics.outputs.issue_avg_time_to_close }}
|
||||
|
||||
**🔀 Pull Requests**
|
||||
• Created: ${{ steps.metrics.outputs.prs_created }}
|
||||
• Merged: ${{ steps.metrics.outputs.prs_merged }}
|
||||
• Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }}
|
||||
• Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
||||
|
||||
**📈 Visual Analytics**
|
||||
https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg
|
||||
**⏱️ Response Times**
|
||||
• First Response: ${{ steps.metrics.outputs.avg_first_response }}
|
||||
• Time to Close: ${{ steps.metrics.outputs.avg_time_to_close }}
|
||||
• PR Merge Time: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
||||
color: 0x58AFFF
|
||||
username: Task Master Metrics Bot
|
||||
avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@manypkg/get-packages@1.1.3/schema.json",
|
||||
"defaultBranch": "main",
|
||||
"ignoredRules": ["ROOT_HAS_DEPENDENCIES", "INTERNAL_MISMATCH"],
|
||||
"ignoredPackages": ["@tm/core", "@tm/cli", "@tm/build-config"]
|
||||
}
|
||||
@@ -85,7 +85,7 @@ Task Master provides an MCP server that Claude Code can connect to. Configure in
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_key_here",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-sonnet-4-20250514",
|
||||
"maxTokens": 64000,
|
||||
"modelId": "claude-3-7-sonnet-20250219",
|
||||
"maxTokens": 120000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"research": {
|
||||
@@ -14,8 +14,8 @@
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-3-7-sonnet-20250219",
|
||||
"maxTokens": 120000,
|
||||
"modelId": "claude-3-5-sonnet-20241022",
|
||||
"maxTokens": 8192,
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
@@ -29,15 +29,9 @@
|
||||
"ollamaBaseURL": "http://localhost:11434/api",
|
||||
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||
"responseLanguage": "English",
|
||||
"enableCodebaseAnalysis": true,
|
||||
"userId": "1234567890",
|
||||
"azureBaseURL": "https://your-endpoint.azure.com/",
|
||||
"defaultTag": "master"
|
||||
},
|
||||
"claudeCode": {},
|
||||
"grokCli": {
|
||||
"timeout": 120000,
|
||||
"workingDirectory": null,
|
||||
"defaultModel": "grok-4-latest"
|
||||
}
|
||||
"claudeCode": {}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<context>
|
||||
# Overview
|
||||
Add a new CLI command: `task-master start <task_id>` (alias: `tm start <task_id>`). This command hard-codes `claude-code` as the executor, fetches task details, builds a standardized prompt, runs claude-code, shows the result, checks for git changes, and auto-marks the task as done if successful.
|
||||
|
||||
We follow the Commander class pattern, reuse task retrieval from `show` command flow. Extremely minimal for 1-hour hackathon timeline.
|
||||
|
||||
# Core Features
|
||||
- `start` command (Commander class style)
|
||||
- Hard-coded executor: `claude-code`
|
||||
- Standardized prompt designed for minimal changes following existing patterns
|
||||
- Shows claude-code output (no streaming)
|
||||
- Git status check for success detection
|
||||
- Auto-mark task done if successful
|
||||
|
||||
# User Experience
|
||||
```
|
||||
task-master start 12
|
||||
```
|
||||
1) Fetches Task #12 details
|
||||
2) Builds standardized prompt with task context
|
||||
3) Runs claude-code with the prompt
|
||||
4) Shows output
|
||||
5) Checks git status for changes
|
||||
6) Auto-marks task done if changes detected
|
||||
</context>
|
||||
|
||||
<PRD>
|
||||
# Technical Architecture
|
||||
|
||||
- Command pattern:
|
||||
- Create `apps/cli/src/commands/start.command.ts` modeled on [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) and task lookup from [show.command.ts](mdc:apps/cli/src/commands/show.command.ts)
|
||||
|
||||
- Task retrieval:
|
||||
- Use `@tm/core` via `createTaskMasterCore` to get task by ID
|
||||
- Extract: id, title, description, details
|
||||
|
||||
- Executor (ultra-simple approach):
|
||||
- Execute `claude "full prompt here"` command directly
|
||||
- The prompt tells Claude to first run `tm show <task_id>` to get task details
|
||||
- Then tells Claude to implement the code changes
|
||||
- This opens Claude CLI interface naturally in the current terminal
|
||||
- No subprocess management needed - just execute the command
|
||||
|
||||
- Execution flow:
|
||||
1) Validate `<task_id>` exists; exit with error if not
|
||||
2) Build standardized prompt that includes instructions to run `tm show <task_id>`
|
||||
3) Execute `claude "prompt"` command directly in terminal
|
||||
4) Claude CLI opens, runs `tm show`, then implements changes
|
||||
5) After Claude session ends, run `git status --porcelain` to detect changes
|
||||
6) If changes detected, auto-run `task-master set-status --id=<task_id> --status=done`
|
||||
|
||||
- Success criteria:
|
||||
- Success = exit code 0 AND git shows modified/created files
|
||||
- Print changed file paths; warn if no changes detected
|
||||
|
||||
# Development Roadmap
|
||||
|
||||
MVP (ship in ~1 hour):
|
||||
1) Implement `start.command.ts` (Commander class), parse `<task_id>`
|
||||
2) Validate task exists via tm-core
|
||||
3) Build prompt that tells Claude to run `tm show <task_id>` then implement
|
||||
4) Execute `claude "prompt"` command, then check git status and auto-mark done
|
||||
|
||||
# Risks and Mitigations
|
||||
- Executor availability: Error clearly if `claude-code` provider fails
|
||||
- False success: Git-change heuristic acceptable for hackathon MVP
|
||||
|
||||
# Appendix
|
||||
|
||||
**Standardized Prompt Template:**
|
||||
```
|
||||
You are an AI coding assistant with access to this repository's codebase.
|
||||
|
||||
First, run this command to get the task details:
|
||||
tm show <task_id>
|
||||
|
||||
Then implement the task with these requirements:
|
||||
- Make the SMALLEST number of code changes possible
|
||||
- Follow ALL existing patterns in the codebase (you have access to analyze the code)
|
||||
- Do NOT over-engineer the solution
|
||||
- Use existing files/functions/patterns wherever possible
|
||||
- When complete, print: COMPLETED: <brief summary of changes>
|
||||
|
||||
Begin by running tm show <task_id> to understand what needs to be implemented.
|
||||
```
|
||||
|
||||
**Key References:**
|
||||
- [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) - Command structure
|
||||
- [show.command.ts](mdc:apps/cli/src/commands/show.command.ts) - Task validation
|
||||
- Node.js `child_process.exec()` - For executing `claude "prompt"` command
|
||||
</PRD>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"currentTag": "master",
|
||||
"lastSwitched": "2025-09-12T22:25:27.535Z",
|
||||
"lastSwitched": "2025-08-27T21:03:20.550Z",
|
||||
"branchTagMapping": {
|
||||
"v017-adds": "v017-adds",
|
||||
"next": "next"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Task ID: 1
|
||||
# Title: Create start command class structure
|
||||
# Status: pending
|
||||
# Dependencies: None
|
||||
# Priority: high
|
||||
# Description: Create the basic structure for the start command following the Commander class pattern
|
||||
# Details:
|
||||
Create a new file `apps/cli/src/commands/start.command.ts` based on the existing list.command.ts pattern. Implement the command class with proper command registration, description, and argument handling for the task_id parameter. The class should extend the base Command class and implement the required methods.
|
||||
|
||||
Example structure:
|
||||
```typescript
|
||||
import { Command } from 'commander';
|
||||
import { BaseCommand } from './base.command';
|
||||
|
||||
export class StartCommand extends BaseCommand {
|
||||
public register(program: Command): void {
|
||||
program
|
||||
.command('start')
|
||||
.alias('tm start')
|
||||
.description('Start implementing a task using claude-code')
|
||||
.argument('<task_id>', 'ID of the task to start')
|
||||
.action(async (taskId: string) => {
|
||||
await this.execute(taskId);
|
||||
});
|
||||
}
|
||||
|
||||
public async execute(taskId: string): Promise<void> {
|
||||
// Implementation will be added in subsequent tasks
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Test Strategy:
|
||||
Verify the command registers correctly by running the CLI with --help and checking that the start command appears with proper description and arguments. Test the basic structure by ensuring the command can be invoked without errors.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Task ID: 2
|
||||
# Title: Register start command in CLI
|
||||
# Status: pending
|
||||
# Dependencies: 7
|
||||
# Priority: high
|
||||
# Description: Register the start command in the CLI application
|
||||
# Details:
|
||||
Update the CLI application to register the new start command. This involves importing the StartCommand class and adding it to the commands array in the CLI initialization.
|
||||
|
||||
In `apps/cli/src/index.ts` or the appropriate file where commands are registered:
|
||||
|
||||
```typescript
|
||||
import { StartCommand } from './commands/start.command';
|
||||
|
||||
// Add StartCommand to the commands array
|
||||
const commands = [
|
||||
// ... existing commands
|
||||
new StartCommand(),
|
||||
];
|
||||
|
||||
// Register all commands
|
||||
commands.forEach(command => command.register(program));
|
||||
```
|
||||
|
||||
# Test Strategy:
|
||||
Verify the command is correctly registered by running the CLI with --help and checking that the start command appears in the list of available commands.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Task ID: 3
|
||||
# Title: Create standardized prompt builder
|
||||
# Status: pending
|
||||
# Dependencies: 1
|
||||
# Priority: medium
|
||||
# Description: Implement a function to build the standardized prompt for claude-code based on the task details
|
||||
# Details:
|
||||
Create a function in the StartCommand class that builds the standardized prompt according to the template provided in the PRD. The prompt should include instructions for Claude to first run `tm show <task_id>` to get task details, and then implement the required changes.
|
||||
|
||||
```typescript
|
||||
private buildPrompt(taskId: string): string {
|
||||
return `You are an AI coding assistant with access to this repository's codebase.
|
||||
|
||||
First, run this command to get the task details:
|
||||
tm show ${taskId}
|
||||
|
||||
Then implement the task with these requirements:
|
||||
- Make the SMALLEST number of code changes possible
|
||||
- Follow ALL existing patterns in the codebase (you have access to analyze the code)
|
||||
- Do NOT over-engineer the solution
|
||||
- Use existing files/functions/patterns wherever possible
|
||||
- When complete, print: COMPLETED: <brief summary of changes>
|
||||
|
||||
Begin by running tm show ${taskId} to understand what needs to be implemented.`;
|
||||
}
|
||||
```
|
||||
<info added on 2025-09-12T02:40:01.812Z>
|
||||
The prompt builder function will handle task context retrieval by instructing Claude to use the task-master show command. This approach ensures Claude has access to all necessary task details before implementation begins. The command syntax "tm show ${taskId}" embedded in the prompt will direct Claude to first gather the complete task context, including description, requirements, and any existing implementation details, before proceeding with code changes.
|
||||
</info added on 2025-09-12T02:40:01.812Z>
|
||||
|
||||
# Test Strategy:
|
||||
Verify the prompt is correctly formatted by calling the function with a sample task ID and checking that the output matches the expected template with the task ID properly inserted.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Task ID: 4
|
||||
# Title: Implement claude-code executor
|
||||
# Status: pending
|
||||
# Dependencies: 3
|
||||
# Priority: high
|
||||
# Description: Add functionality to execute the claude-code command with the built prompt
|
||||
# Details:
|
||||
Implement the functionality to execute the claude command with the built prompt. This should use Node.js child_process.exec() to run the command directly in the terminal.
|
||||
|
||||
```typescript
|
||||
import { exec } from 'child_process';
|
||||
|
||||
// Inside execute method, after task validation
|
||||
private async executeClaude(prompt: string): Promise<void> {
|
||||
console.log('Starting claude-code to implement the task...');
|
||||
|
||||
try {
|
||||
// Execute claude with the prompt
|
||||
const claudeCommand = `claude "${prompt.replace(/"/g, '\\"')}"`;
|
||||
|
||||
// Use execSync to wait for the command to complete
|
||||
const { execSync } = require('child_process');
|
||||
execSync(claudeCommand, { stdio: 'inherit' });
|
||||
|
||||
console.log('Claude session completed.');
|
||||
} catch (error) {
|
||||
console.error('Error executing claude-code:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then call this method from the execute method after building the prompt.
|
||||
|
||||
# Test Strategy:
|
||||
Test by running the command with a valid task ID and verifying that the claude command is executed with the correct prompt. Check that the command handles errors appropriately if claude-code is not available.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Task ID: 7
|
||||
# Title: Integrate execution flow in start command
|
||||
# Status: pending
|
||||
# Dependencies: 3, 4
|
||||
# Priority: high
|
||||
# Description: Connect all the components to implement the complete execution flow for the start command
|
||||
# Details:
|
||||
Update the execute method in the StartCommand class to integrate all the components and implement the complete execution flow as described in the PRD:
|
||||
1. Validate task exists
|
||||
2. Build standardized prompt
|
||||
3. Execute claude-code
|
||||
4. Check git status for changes
|
||||
5. Auto-mark task as done if changes detected
|
||||
|
||||
```typescript
|
||||
public async execute(taskId: string): Promise<void> {
|
||||
// Validate task exists
|
||||
const core = await createTaskMasterCore();
|
||||
const task = await core.tasks.getById(parseInt(taskId, 10));
|
||||
|
||||
if (!task) {
|
||||
console.error(`Task with ID ${taskId} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build prompt
|
||||
const prompt = this.buildPrompt(taskId);
|
||||
|
||||
// Execute claude-code
|
||||
await this.executeClaude(prompt);
|
||||
|
||||
// Check git status
|
||||
const changedFiles = await this.checkGitChanges();
|
||||
|
||||
if (changedFiles.length > 0) {
|
||||
console.log('\nChanges detected in the following files:');
|
||||
changedFiles.forEach(file => console.log(`- ${file}`));
|
||||
|
||||
// Auto-mark task as done
|
||||
await this.markTaskAsDone(taskId);
|
||||
console.log(`\nTask ${taskId} completed successfully and marked as done.`);
|
||||
} else {
|
||||
console.warn('\nNo changes detected after claude-code execution. Task not marked as done.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Test Strategy:
|
||||
Test the complete execution flow by running the start command with a valid task ID and verifying that all steps are executed correctly. Test with both scenarios: when changes are detected and when no changes are detected.
|
||||
File diff suppressed because one or more lines are too long
157
CHANGELOG.md
157
CHANGELOG.md
@@ -1,162 +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
|
||||
|
||||
- [#1248](https://github.com/eyaltoledano/claude-task-master/pull/1248) [`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix set-status for subtasks:
|
||||
- Parent tasks are now set as `done` when subtasks are all `done`
|
||||
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
|
||||
|
||||
## 0.27.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1232](https://github.com/eyaltoledano/claude-task-master/pull/1232) [`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix module not found for new 0.27.0 release
|
||||
|
||||
- [#1233](https://github.com/eyaltoledano/claude-task-master/pull/1233) [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix Zed MCP configuration by adding required "source" property
|
||||
- Add "source": "custom" property to task-master-ai server in Zed settings.json
|
||||
|
||||
## 0.27.1-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1233](https://github.com/eyaltoledano/claude-task-master/pull/1233) [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - One last testing final final
|
||||
|
||||
## 0.27.1-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1232](https://github.com/eyaltoledano/claude-task-master/pull/1232) [`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix module not found for new 0.27.0 release
|
||||
|
||||
## 0.27.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1220](https://github.com/eyaltoledano/claude-task-master/pull/1220) [`4e12643`](https://github.com/eyaltoledano/claude-task-master/commit/4e126430a092fb54afb035514fb3d46115714f97) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - No longer need --package=task-master-ai in mcp server
|
||||
- A lot of users were having issues with Taskmaster and usually a simple fix was to remove --package from your mcp.json
|
||||
- we now bundle our whole package, so we no longer need the --package
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add new `task-master start` command for automated task execution with Claude Code
|
||||
- You can now start working on tasks directly by running `task-master start <task-id>` which will automatically launch Claude Code with a comprehensive prompt containing all task details, implementation guidelines, and context.
|
||||
- `task-master start` will automatically detect next-task when no ID is provided.
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Move from javascript to typescript, not a full refactor but we now have a typescript environment and are moving our javascript commands slowly into typescript
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add grok-cli as a provider with full codebase context support. You can now use Grok models (grok-2, grok-3, grok-4, etc.) with Task Master for AI operations that have access to your entire codebase context, enabling more informed task generation and PRD parsing.
|
||||
|
||||
## Setup Instructions
|
||||
1. **Get your Grok API key** from [console.x.ai](https://console.x.ai)
|
||||
2. **Set the environment variable**:
|
||||
```bash
|
||||
export GROK_CLI_API_KEY="your-api-key-here"
|
||||
```
|
||||
3. **Configure Task Master to use Grok**:
|
||||
```bash
|
||||
task-master models --set-main grok-beta
|
||||
# or
|
||||
task-master models --set-research grok-beta
|
||||
# or
|
||||
task-master models --set-fallback grok-beta
|
||||
```
|
||||
|
||||
## Key Features
|
||||
- **Full codebase context**: Grok models can analyze your entire project when generating tasks or parsing PRDs
|
||||
- **xAI model access**: Support for latest Grok models (grok-2, grok-3, grok-4, etc.)
|
||||
- **Code-aware task generation**: Create more accurate and contextual tasks based on your actual codebase
|
||||
- **Intelligent PRD parsing**: Parse requirements with understanding of your existing code structure
|
||||
|
||||
## Available Models
|
||||
- `grok-beta` - Latest Grok model with codebase context
|
||||
- `grok-vision-beta` - Grok with vision capabilities and codebase context
|
||||
|
||||
The Grok CLI provider integrates with xAI's Grok models via grok-cli and can also use the local Grok CLI configuration file (`~/.grok/user-settings.json`) if available.
|
||||
|
||||
## Credits
|
||||
|
||||
Built using the [grok-cli](https://github.com/superagent-ai/grok-cli) by Superagent AI for seamless integration with xAI's Grok models.
|
||||
|
||||
- [#1225](https://github.com/eyaltoledano/claude-task-master/pull/1225) [`a621ff0`](https://github.com/eyaltoledano/claude-task-master/commit/a621ff05eafb51a147a9aabd7b37ddc0e45b0869) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve taskmaster ai provider defaults
|
||||
- moving from main anthropic 3.7 to anthropic sonnet 4
|
||||
- moving from fallback anthropic 3.5 to anthropic 3.7
|
||||
|
||||
- [#1217](https://github.com/eyaltoledano/claude-task-master/pull/1217) [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - @tm/cli: add auto-update functionality to every command
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Fix Grok model configuration validation and update deprecated Claude fallback model. Grok models now properly support their full 131K token capacity, and the fallback model has been upgraded to Claude Sonnet 4 for better performance and future compatibility.
|
||||
|
||||
## 0.27.0-rc.2
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1217](https://github.com/eyaltoledano/claude-task-master/pull/1217) [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - @tm/cli: add auto-update functionality to every command
|
||||
|
||||
## 0.27.0-rc.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`255b9f0`](https://github.com/eyaltoledano/claude-task-master/commit/255b9f0334555b0063280abde701445cd62fa11b) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Testing one more pre-release iteration
|
||||
|
||||
## 0.27.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Test out the RC
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
|
||||
- @tm/cli@0.27.0-rc.0
|
||||
|
||||
## 0.26.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1133](https://github.com/eyaltoledano/claude-task-master/pull/1133) [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144) Thanks [@neonwatty](https://github.com/neonwatty)! - Restore Taskmaster claude-code commands and move clear commands under /remove to avoid collision with the claude-code /clear command.
|
||||
|
||||
- [#1163](https://github.com/eyaltoledano/claude-task-master/pull/1163) [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Gemini CLI provider with codebase-aware task generation
|
||||
|
||||
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
|
||||
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
|
||||
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
|
||||
|
||||
- [#1165](https://github.com/eyaltoledano/claude-task-master/pull/1165) [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add configurable codebase analysis feature flag with multiple configuration sources
|
||||
|
||||
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
|
||||
|
||||
Priority order: .env > MCP session env > .taskmaster/config.json.
|
||||
|
||||
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.
|
||||
|
||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - feat(move): improve cross-tag move UX and safety
|
||||
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
|
||||
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
|
||||
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
|
||||
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
|
||||
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
|
||||
|
||||
***
|
||||
|
||||
- [#1162](https://github.com/eyaltoledano/claude-task-master/pull/1162) [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
|
||||
|
||||
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
|
||||
|
||||
Commands contextualised:
|
||||
- add-task
|
||||
- update-subtask
|
||||
- update-task
|
||||
- update
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
||||
|
||||
## 0.26.0-rc.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -102,7 +102,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
@@ -122,7 +122,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
|
||||
> 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
|
||||
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, restart your editor and check that your API keys are correctly configured.
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, try removing the `--package=task-master-ai` flag from `args`.
|
||||
|
||||
###### VS Code (`servers` + `type`)
|
||||
|
||||
@@ -131,7 +131,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"servers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# @tm/cli
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
## 0.27.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@0.26.1
|
||||
|
||||
## 0.27.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
||||
|
||||
## 1.1.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`cd90b4d`](https://github.com/eyaltoledano/claude-task-master/commit/cd90b4d65fc2f04bdad9fb73aba320b58a124240) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@tm/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "Task Master CLI - Command line interface for task management",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
@@ -23,12 +23,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"boxen": "^8.0.1",
|
||||
"boxen": "^7.1.1",
|
||||
"chalk": "5.6.2",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^12.1.0",
|
||||
"inquirer": "^12.5.0",
|
||||
"ora": "^8.2.0"
|
||||
"inquirer": "^9.2.10",
|
||||
"ora": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import ora, { Ora } from 'ora';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
AuthManager,
|
||||
AuthenticationError,
|
||||
@@ -49,15 +49,8 @@ export class ContextCommand extends Command {
|
||||
this.addClearCommand();
|
||||
this.addSetCommand();
|
||||
|
||||
// Accept optional positional argument for brief ID or Hamster URL
|
||||
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
|
||||
|
||||
// Default action: if an argument is provided, resolve and set context; else show
|
||||
this.action(async (briefOrUrl?: string) => {
|
||||
if (briefOrUrl && briefOrUrl.trim().length > 0) {
|
||||
await this.executeSetFromBriefInput(briefOrUrl.trim());
|
||||
return;
|
||||
}
|
||||
// Default action shows current context
|
||||
this.action(async () => {
|
||||
await this.executeShow();
|
||||
});
|
||||
}
|
||||
@@ -333,7 +326,7 @@ export class ContextCommand extends Command {
|
||||
choices: [
|
||||
{ name: '(No brief - organization level)', value: null },
|
||||
...briefs.map((brief) => ({
|
||||
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||
name: `Brief ${brief.id.slice(0, 8)} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||
value: brief
|
||||
}))
|
||||
]
|
||||
@@ -448,142 +441,6 @@ export class ContextCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute setting context from a brief ID or Hamster URL
|
||||
*/
|
||||
private async executeSetFromBriefInput(briefOrUrl: string): Promise<void> {
|
||||
let spinner: Ora | undefined;
|
||||
try {
|
||||
// Check authentication
|
||||
if (!this.authManager.isAuthenticated()) {
|
||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner = ora('Resolving brief...');
|
||||
spinner.start();
|
||||
|
||||
// Extract brief ID
|
||||
const briefId = this.extractBriefId(briefOrUrl);
|
||||
if (!briefId) {
|
||||
spinner.fail('Could not extract a brief ID from the provided input');
|
||||
ui.displayError(
|
||||
`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fetch brief and resolve its organization
|
||||
const brief = await this.authManager.getBrief(briefId);
|
||||
if (!brief) {
|
||||
spinner.fail('Brief not found or you do not have access');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Fetch org to get a friendly name (optional)
|
||||
let orgName: string | undefined;
|
||||
try {
|
||||
const org = await this.authManager.getOrganization(brief.accountId);
|
||||
orgName = org?.name;
|
||||
} catch {
|
||||
// Non-fatal if org lookup fails
|
||||
}
|
||||
|
||||
// Update context: set org and brief
|
||||
const briefName = `Brief ${brief.id.slice(0, 8)}`;
|
||||
await this.authManager.updateContext({
|
||||
orgId: brief.accountId,
|
||||
orgName,
|
||||
briefId: brief.id,
|
||||
briefName
|
||||
});
|
||||
|
||||
spinner.succeed('Context set from brief');
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}`
|
||||
)
|
||||
);
|
||||
|
||||
this.setLastResult({
|
||||
success: true,
|
||||
action: 'set',
|
||||
context: this.authManager.getContext() || undefined,
|
||||
message: 'Context set from brief'
|
||||
});
|
||||
} catch (error: any) {
|
||||
try {
|
||||
if (spinner?.isSpinning) spinner.stop();
|
||||
} catch {}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a brief ID from raw input (ID or Hamster URL)
|
||||
*/
|
||||
private extractBriefId(input: string): string | null {
|
||||
const raw = input?.trim() ?? '';
|
||||
if (!raw) return null;
|
||||
|
||||
const parseUrl = (s: string): URL | null => {
|
||||
try {
|
||||
return new URL(s);
|
||||
} catch {}
|
||||
try {
|
||||
return new URL(`https://${s}`);
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fromParts = (path: string): string | null => {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const briefsIdx = parts.lastIndexOf('briefs');
|
||||
const candidate =
|
||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
||||
? parts[briefsIdx + 1]
|
||||
: parts[parts.length - 1];
|
||||
return candidate?.trim() || null;
|
||||
};
|
||||
|
||||
// 1) URL (absolute or scheme‑less)
|
||||
const url = parseUrl(raw);
|
||||
if (url) {
|
||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
||||
if (candidate) {
|
||||
// Light sanity check; let API be the final validator
|
||||
if (this.isLikelyId(candidate) || candidate.length >= 8)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Looks like a path without scheme
|
||||
if (raw.includes('/')) {
|
||||
const candidate = fromParts(raw);
|
||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: raw token
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic to check if a string looks like a brief ID (UUID-like)
|
||||
*/
|
||||
private isLikelyId(value: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; // ULID
|
||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/; // general token
|
||||
return (
|
||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context directly from options
|
||||
*/
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
/**
|
||||
* @fileoverview SetStatusCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
type TaskStatus
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
|
||||
/**
|
||||
* Valid task status values for validation
|
||||
*/
|
||||
const VALID_TASK_STATUSES: TaskStatus[] = [
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked',
|
||||
'review'
|
||||
];
|
||||
|
||||
/**
|
||||
* Options interface for the set-status command
|
||||
*/
|
||||
export interface SetStatusCommandOptions {
|
||||
id?: string;
|
||||
status?: TaskStatus;
|
||||
format?: 'text' | 'json';
|
||||
silent?: boolean;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result type from set-status command
|
||||
*/
|
||||
export interface SetStatusResult {
|
||||
success: boolean;
|
||||
updatedTasks: Array<{
|
||||
taskId: string;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
}>;
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetStatusCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class SetStatusCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private lastResult?: SetStatusResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'set-status');
|
||||
|
||||
// Configure the command
|
||||
this.description('Update the status of one or more tasks')
|
||||
.requiredOption(
|
||||
'-i, --id <id>',
|
||||
'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)'
|
||||
)
|
||||
.requiredOption(
|
||||
'-s, --status <status>',
|
||||
`New status (${VALID_TASK_STATUSES.join(', ')})`
|
||||
)
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.action(async (options: SetStatusCommandOptions) => {
|
||||
await this.executeCommand(options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the set-status command
|
||||
*/
|
||||
private async executeCommand(
|
||||
options: SetStatusCommandOptions
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Validate required options
|
||||
if (!options.id) {
|
||||
console.error(chalk.red('Error: Task ID is required. Use -i or --id'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.error(
|
||||
chalk.red('Error: Status is required. Use -s or --status')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if (!VALID_TASK_STATUSES.includes(options.status)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize TaskMaster core
|
||||
this.tmCore = await createTaskMasterCore({
|
||||
projectPath: options.project || process.cwd()
|
||||
});
|
||||
|
||||
// Parse task IDs (handle comma-separated values)
|
||||
const taskIds = options.id.split(',').map((id) => id.trim());
|
||||
|
||||
// Update each task
|
||||
const updatedTasks: Array<{
|
||||
taskId: string;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
}> = [];
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
try {
|
||||
const result = await this.tmCore.updateTaskStatus(
|
||||
taskId,
|
||||
options.status
|
||||
);
|
||||
updatedTasks.push({
|
||||
taskId: result.taskId,
|
||||
oldStatus: result.oldStatus,
|
||||
newStatus: result.newStatus
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (!options.silent) {
|
||||
console.error(
|
||||
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
||||
);
|
||||
}
|
||||
if (options.format === 'json') {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
taskId,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Store result for potential reuse
|
||||
this.lastResult = {
|
||||
success: true,
|
||||
updatedTasks,
|
||||
storageType: this.tmCore.getStorageType() as Exclude<
|
||||
StorageType,
|
||||
'auto'
|
||||
>
|
||||
};
|
||||
|
||||
// Display results
|
||||
this.displayResults(this.lastResult, options);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
if (!options.silent) {
|
||||
console.error(chalk.red(`Error: ${errorMessage}`));
|
||||
}
|
||||
|
||||
if (options.format === 'json') {
|
||||
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up resources
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: SetStatusResult,
|
||||
options: SetStatusCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
if (!options.silent) {
|
||||
this.displayTextResults(result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results in text format
|
||||
*/
|
||||
private displayTextResults(result: SetStatusResult): void {
|
||||
if (result.updatedTasks.length === 1) {
|
||||
// Single task update
|
||||
const update = result.updatedTasks[0];
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) +
|
||||
'\n\n' +
|
||||
`${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` +
|
||||
`${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`,
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Multiple task updates
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold(
|
||||
`✅ Successfully updated ${result.updatedTasks.length} tasks`
|
||||
) +
|
||||
'\n\n' +
|
||||
result.updatedTasks
|
||||
.map(
|
||||
(update) =>
|
||||
`${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}`
|
||||
)
|
||||
.join('\n'),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Show storage info
|
||||
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colored status display
|
||||
*/
|
||||
private getStatusDisplay(status: TaskStatus): string {
|
||||
const statusColors: Record<TaskStatus, (text: string) => string> = {
|
||||
pending: chalk.yellow,
|
||||
'in-progress': chalk.blue,
|
||||
done: chalk.green,
|
||||
deferred: chalk.gray,
|
||||
cancelled: chalk.red,
|
||||
blocked: chalk.red,
|
||||
review: chalk.magenta,
|
||||
completed: chalk.green
|
||||
};
|
||||
|
||||
const colorFn = statusColors[status] || chalk.white;
|
||||
return colorFn(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last command result (useful for testing or chaining)
|
||||
*/
|
||||
getLastResult(): SetStatusResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
* This is for gradual migration - allows commands.js to use this
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const setStatusCommand = new SetStatusCommand();
|
||||
program.addCommand(setStatusCommand);
|
||||
return setStatusCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
* Can also configure the command name if needed
|
||||
*/
|
||||
static register(program: Command, name?: string): SetStatusCommand {
|
||||
const setStatusCommand = new SetStatusCommand(name);
|
||||
program.addCommand(setStatusCommand);
|
||||
return setStatusCommand;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create and configure the set-status command
|
||||
*/
|
||||
export function createSetStatusCommand(): SetStatusCommand {
|
||||
return new SetStatusCommand();
|
||||
}
|
||||
@@ -9,7 +9,14 @@ import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import {
|
||||
displayTaskHeader,
|
||||
displayTaskProperties,
|
||||
displayImplementationDetails,
|
||||
displayTestStrategy,
|
||||
displaySubtasks,
|
||||
displaySuggestedActions
|
||||
} from '../ui/components/task-detail.component.js';
|
||||
|
||||
/**
|
||||
* Options interface for the show command
|
||||
@@ -257,11 +264,44 @@ export class ShowCommand extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the global task details display function
|
||||
displayTaskDetails(result.task, {
|
||||
statusFilter: options.status,
|
||||
showSuggestedActions: true
|
||||
});
|
||||
const task = result.task;
|
||||
|
||||
// Display header with tag
|
||||
displayTaskHeader(task.id, task.title);
|
||||
|
||||
// Display task properties in table format
|
||||
displayTaskProperties(task);
|
||||
|
||||
// Display implementation details if available
|
||||
if (task.details) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayImplementationDetails(task.details);
|
||||
}
|
||||
|
||||
// Display test strategy if available
|
||||
if ('testStrategy' in task && task.testStrategy) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayTestStrategy(task.testStrategy as string);
|
||||
}
|
||||
|
||||
// Display subtasks if available
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
// Filter subtasks by status if provided
|
||||
const filteredSubtasks = options.status
|
||||
? task.subtasks.filter((sub) => sub.status === options.status)
|
||||
: task.subtasks;
|
||||
|
||||
if (filteredSubtasks.length === 0 && options.status) {
|
||||
console.log(
|
||||
chalk.gray(` No subtasks with status '${options.status}'`)
|
||||
);
|
||||
} else {
|
||||
displaySubtasks(filteredSubtasks, task.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Display suggested actions
|
||||
displaySuggestedActions(task.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,512 +0,0 @@
|
||||
/**
|
||||
* @fileoverview StartCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
* This is a thin presentation layer over @tm/core's TaskExecutionService
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import ora, { type Ora } from 'ora';
|
||||
import { spawn } from 'child_process';
|
||||
import {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCore,
|
||||
type StartTaskResult as CoreStartTaskResult
|
||||
} from '@tm/core';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
* CLI-specific options interface for the start command
|
||||
*/
|
||||
export interface StartCommandOptions {
|
||||
id?: string;
|
||||
format?: 'text' | 'json';
|
||||
project?: string;
|
||||
dryRun?: boolean;
|
||||
force?: boolean;
|
||||
noStatusUpdate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-specific result type from start command
|
||||
* Extends the core result with CLI-specific display information
|
||||
*/
|
||||
export interface StartCommandResult extends CoreStartTaskResult {
|
||||
storageType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* StartCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core's TaskExecutionService
|
||||
*/
|
||||
export class StartCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private lastResult?: StartCommandResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'start');
|
||||
|
||||
// Configure the command
|
||||
this.description(
|
||||
'Start working on a task by launching claude-code with context'
|
||||
)
|
||||
.argument('[id]', 'Task ID to start working on')
|
||||
.option('-i, --id <id>', 'Task ID to start working on')
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.option(
|
||||
'--dry-run',
|
||||
'Show what would be executed without launching claude-code'
|
||||
)
|
||||
.option(
|
||||
'--force',
|
||||
'Force start even if another task is already in-progress'
|
||||
)
|
||||
.option(
|
||||
'--no-status-update',
|
||||
'Do not automatically update task status to in-progress'
|
||||
)
|
||||
.action(
|
||||
async (taskId: string | undefined, options: StartCommandOptions) => {
|
||||
await this.executeCommand(taskId, options);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the start command
|
||||
*/
|
||||
private async executeCommand(
|
||||
taskId: string | undefined,
|
||||
options: StartCommandOptions
|
||||
): Promise<void> {
|
||||
let spinner: Ora | null = null;
|
||||
|
||||
try {
|
||||
// Validate options
|
||||
if (!this.validateOptions(options)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize tm-core with spinner
|
||||
spinner = ora('Initializing Task Master...').start();
|
||||
await this.initializeCore(options.project || process.cwd());
|
||||
spinner.succeed('Task Master initialized');
|
||||
|
||||
// Get the task ID from argument or option, or find next available task
|
||||
const idArg = taskId || options.id || null;
|
||||
let targetTaskId = idArg;
|
||||
|
||||
if (!targetTaskId) {
|
||||
spinner = ora('Finding next available task...').start();
|
||||
targetTaskId = await this.performGetNextTask();
|
||||
if (targetTaskId) {
|
||||
spinner.succeed(`Found next task: #${targetTaskId}`);
|
||||
} else {
|
||||
spinner.fail('No available tasks found');
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetTaskId) {
|
||||
ui.displayError('No task ID provided and no available tasks found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Show pre-launch message (no spinner needed, it's just display)
|
||||
if (!options.dryRun) {
|
||||
await this.showPreLaunchMessage(targetTaskId);
|
||||
}
|
||||
|
||||
// Use tm-core's startTask method with spinner
|
||||
spinner = ora('Preparing task execution...').start();
|
||||
const coreResult = await this.performStartTask(targetTaskId, options);
|
||||
|
||||
if (coreResult.started) {
|
||||
spinner.succeed(
|
||||
options.dryRun
|
||||
? 'Dry run completed'
|
||||
: 'Task prepared - launching Claude...'
|
||||
);
|
||||
} else {
|
||||
spinner.fail('Task execution failed');
|
||||
}
|
||||
|
||||
// Execute command if we have one and it's not a dry run
|
||||
if (!options.dryRun && coreResult.command) {
|
||||
// Stop any remaining spinners before launching Claude
|
||||
if (spinner && !spinner.isSpinning) {
|
||||
// Clear the line to make room for Claude
|
||||
console.log();
|
||||
}
|
||||
await this.executeChildProcess(coreResult.command);
|
||||
}
|
||||
|
||||
// Convert core result to CLI result with storage type
|
||||
const result: StartCommandResult = {
|
||||
...coreResult,
|
||||
storageType: this.tmCore?.getStorageType()
|
||||
};
|
||||
|
||||
// Store result for programmatic access
|
||||
this.setLastResult(result);
|
||||
|
||||
// Display results (only for dry run or if execution failed)
|
||||
if (options.dryRun || !coreResult.started) {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (spinner) {
|
||||
spinner.fail('Operation failed');
|
||||
}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command options
|
||||
*/
|
||||
private validateOptions(options: StartCommandOptions): boolean {
|
||||
// Validate format
|
||||
if (options.format && !['text', 'json'].includes(options.format)) {
|
||||
console.error(chalk.red(`Invalid format: ${options.format}`));
|
||||
console.error(chalk.gray(`Valid formats: text, json`));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available task using tm-core
|
||||
*/
|
||||
private async performGetNextTask(): Promise<string | null> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
}
|
||||
return this.tmCore.getNextAvailableTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show pre-launch message using tm-core data
|
||||
*/
|
||||
private async showPreLaunchMessage(targetTaskId: string): Promise<void> {
|
||||
if (!this.tmCore) return;
|
||||
|
||||
const { task, subtask, subtaskId } =
|
||||
await this.tmCore.getTaskWithSubtask(targetTaskId);
|
||||
if (task) {
|
||||
const workItemText = subtask
|
||||
? `Subtask #${task.id}.${subtaskId} - ${subtask.title}`
|
||||
: `Task #${task.id} - ${task.title}`;
|
||||
|
||||
console.log(
|
||||
chalk.green('🚀 Starting: ') + chalk.white.bold(workItemText)
|
||||
);
|
||||
console.log(chalk.gray('Launching Claude Code...'));
|
||||
console.log(); // Empty line
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform start task using tm-core business logic
|
||||
*/
|
||||
private async performStartTask(
|
||||
targetTaskId: string,
|
||||
options: StartCommandOptions
|
||||
): Promise<CoreStartTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
}
|
||||
|
||||
// Show spinner for status update if enabled
|
||||
let statusSpinner: Ora | null = null;
|
||||
if (!options.noStatusUpdate && !options.dryRun) {
|
||||
statusSpinner = ora('Updating task status to in-progress...').start();
|
||||
}
|
||||
|
||||
// Get execution command from tm-core (instead of executing directly)
|
||||
const result = await this.tmCore.startTask(targetTaskId, {
|
||||
dryRun: options.dryRun,
|
||||
force: options.force,
|
||||
updateStatus: !options.noStatusUpdate
|
||||
});
|
||||
|
||||
if (statusSpinner) {
|
||||
if (result.started) {
|
||||
statusSpinner.succeed('Task status updated');
|
||||
} else {
|
||||
statusSpinner.warn('Task status update skipped');
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Failed to start task - core result is undefined');
|
||||
}
|
||||
|
||||
// Don't execute here - let the main executeCommand method handle it
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the child process directly in the main thread for better process control
|
||||
*/
|
||||
private async executeChildProcess(command: {
|
||||
executable: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Don't show the full command with args as it can be very long
|
||||
console.log(chalk.green('🚀 Launching Claude Code...'));
|
||||
console.log(); // Add space before Claude takes over
|
||||
|
||||
const childProcess = spawn(command.executable, command.args, {
|
||||
cwd: command.cwd,
|
||||
stdio: 'inherit', // Inherit stdio from parent process
|
||||
shell: false
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
reject(new Error(`Failed to spawn process: ${error.message}`));
|
||||
});
|
||||
|
||||
// Handle process termination signals gracefully
|
||||
const cleanup = () => {
|
||||
if (childProcess && !childProcess.killed) {
|
||||
childProcess.kill('SIGTERM');
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
process.on('exit', cleanup);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: StartCommandResult,
|
||||
options: StartCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
this.displayJson(result);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
this.displayTextResult(result, options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in JSON format
|
||||
*/
|
||||
private displayJson(result: StartCommandResult): void {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display result in text format
|
||||
*/
|
||||
private displayTextResult(
|
||||
result: StartCommandResult,
|
||||
options: StartCommandOptions
|
||||
): void {
|
||||
if (!result.found || !result.task) {
|
||||
console.log(
|
||||
boxen(chalk.yellow(`Task not found!`), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = result.task;
|
||||
|
||||
if (options.dryRun) {
|
||||
// For dry run, show full details since Claude Code won't be launched
|
||||
let headerText = `Dry Run: Starting Task #${task.id} - ${task.title}`;
|
||||
|
||||
// If working on a specific subtask, highlight it in the header
|
||||
if (result.subtask && result.subtaskId) {
|
||||
headerText = `Dry Run: Starting Subtask #${task.id}.${result.subtaskId} - ${result.subtask.title}`;
|
||||
}
|
||||
|
||||
displayTaskDetails(task, {
|
||||
customHeader: headerText,
|
||||
headerColor: 'yellow'
|
||||
});
|
||||
|
||||
// Show claude-code prompt
|
||||
if (result.executionOutput) {
|
||||
console.log(); // Empty line for spacing
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Claude-Code Prompt:') +
|
||||
'\n\n' +
|
||||
result.executionOutput,
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'cyan',
|
||||
width: process.stdout.columns * 0.95 || 100
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(); // Empty line for spacing
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.yellow(
|
||||
'🔍 Dry run - claude-code would be launched with the above prompt'
|
||||
),
|
||||
{
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// For actual execution, show minimal info since Claude Code will clear the terminal
|
||||
if (result.started) {
|
||||
// Determine what was worked on - task or subtask
|
||||
let workItemText = `Task: #${task.id} - ${task.title}`;
|
||||
let statusTarget = task.id;
|
||||
|
||||
if (result.subtask && result.subtaskId) {
|
||||
workItemText = `Subtask: #${task.id}.${result.subtaskId} - ${result.subtask.title}`;
|
||||
statusTarget = `${task.id}.${result.subtaskId}`;
|
||||
}
|
||||
|
||||
// Post-execution message (shown after Claude Code exits)
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold('🎉 Task Session Complete!') +
|
||||
'\n\n' +
|
||||
chalk.white(workItemText) +
|
||||
'\n\n' +
|
||||
chalk.cyan('Next steps:') +
|
||||
'\n' +
|
||||
`• Run ${chalk.yellow('tm show ' + task.id)} to review task details\n` +
|
||||
`• Run ${chalk.yellow('tm set-status --id=' + statusTarget + ' --status=done')} when complete\n` +
|
||||
`• Run ${chalk.yellow('tm next')} to find the next available task\n` +
|
||||
`• Run ${chalk.yellow('tm start')} to begin the next task`,
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'green',
|
||||
width: process.stdout.columns * 0.95 || 100,
|
||||
margin: { top: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Error case
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.red(
|
||||
'❌ Failed to launch claude-code' +
|
||||
(result.error ? `\nError: ${result.error}` : '')
|
||||
),
|
||||
{
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'red',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general errors
|
||||
*/
|
||||
private handleError(error: any): void {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
message: error?.message ?? String(error)
|
||||
};
|
||||
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
||||
|
||||
// Show stack trace in development mode or when DEBUG is set
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
if ((isDevelopment || process.env.DEBUG) && error.stack) {
|
||||
console.error(chalk.gray(error.stack));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last result for programmatic access
|
||||
*/
|
||||
private setLastResult(result: StartCommandResult): void {
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result (for programmatic usage)
|
||||
*/
|
||||
getLastResult(): StartCommandResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to register this command on an existing program
|
||||
*/
|
||||
static registerOn(program: Command): Command {
|
||||
const startCommand = new StartCommand();
|
||||
program.addCommand(startCommand);
|
||||
return startCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative registration that returns the command for chaining
|
||||
*/
|
||||
static register(program: Command, name?: string): StartCommand {
|
||||
const startCommand = new StartCommand(name);
|
||||
program.addCommand(startCommand);
|
||||
return startCommand;
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,10 @@ export { ListTasksCommand } from './commands/list.command.js';
|
||||
export { ShowCommand } from './commands/show.command.js';
|
||||
export { AuthCommand } from './commands/auth.command.js';
|
||||
export { ContextCommand } from './commands/context.command.js';
|
||||
export { StartCommand } from './commands/start.command.js';
|
||||
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||
|
||||
// UI utilities (for other commands to use)
|
||||
export * as ui from './utils/ui.js';
|
||||
|
||||
// Auto-update utilities
|
||||
export {
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification,
|
||||
compareVersions
|
||||
} from './utils/auto-update.js';
|
||||
|
||||
// Re-export commonly used types from tm-core
|
||||
export type {
|
||||
Task,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import figlet from 'figlet';
|
||||
import gradient from 'gradient-string';
|
||||
|
||||
/**
|
||||
* Header configuration options
|
||||
|
||||
@@ -262,74 +262,3 @@ export function displaySuggestedActions(taskId: string | number): void {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display complete task details - used by both show and start commands
|
||||
*/
|
||||
export function displayTaskDetails(
|
||||
task: Task,
|
||||
options?: {
|
||||
statusFilter?: string;
|
||||
showSuggestedActions?: boolean;
|
||||
customHeader?: string;
|
||||
headerColor?: string;
|
||||
}
|
||||
): void {
|
||||
const {
|
||||
statusFilter,
|
||||
showSuggestedActions = false,
|
||||
customHeader,
|
||||
headerColor = 'blue'
|
||||
} = options || {};
|
||||
|
||||
// Display header - either custom or default
|
||||
if (customHeader) {
|
||||
console.log(
|
||||
boxen(chalk.white.bold(customHeader), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: headerColor,
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
})
|
||||
);
|
||||
} else {
|
||||
displayTaskHeader(task.id, task.title);
|
||||
}
|
||||
|
||||
// Display task properties in table format
|
||||
displayTaskProperties(task);
|
||||
|
||||
// Display implementation details if available
|
||||
if (task.details) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayImplementationDetails(task.details);
|
||||
}
|
||||
|
||||
// Display test strategy if available
|
||||
if ('testStrategy' in task && task.testStrategy) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayTestStrategy(task.testStrategy as string);
|
||||
}
|
||||
|
||||
// Display subtasks if available
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
// Filter subtasks by status if provided
|
||||
const filteredSubtasks = statusFilter
|
||||
? task.subtasks.filter((sub) => sub.status === statusFilter)
|
||||
: task.subtasks;
|
||||
|
||||
if (filteredSubtasks.length === 0 && statusFilter) {
|
||||
console.log(); // Empty line for spacing
|
||||
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
||||
} else if (filteredSubtasks.length > 0) {
|
||||
console.log(); // Empty line for spacing
|
||||
displaySubtasks(filteredSubtasks, task.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Display suggested actions if requested
|
||||
if (showSuggestedActions) {
|
||||
console.log(); // Empty line for spacing
|
||||
displaySuggestedActions(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Auto-update utilities for task-master-ai CLI
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import https from 'https';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import boxen from 'boxen';
|
||||
|
||||
export interface UpdateInfo {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
needsUpdate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version from build-time injected environment variable
|
||||
*/
|
||||
function getCurrentVersion(): string {
|
||||
// Version is injected at build time via TM_PUBLIC_VERSION
|
||||
const version = process.env.TM_PUBLIC_VERSION;
|
||||
if (version && version !== 'unknown') {
|
||||
return version;
|
||||
}
|
||||
|
||||
// Fallback for development or if injection failed
|
||||
console.warn('Could not read version from TM_PUBLIC_VERSION, using fallback');
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semantic versions with proper pre-release handling
|
||||
* @param v1 - First version
|
||||
* @param v2 - Second version
|
||||
* @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
|
||||
*/
|
||||
export function compareVersions(v1: string, v2: string): number {
|
||||
const toParts = (v: string) => {
|
||||
const [core, pre = ''] = v.split('-', 2);
|
||||
const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
||||
return { nums, pre };
|
||||
};
|
||||
|
||||
const a = toParts(v1);
|
||||
const b = toParts(v2);
|
||||
const len = Math.max(a.nums.length, b.nums.length);
|
||||
|
||||
// Compare numeric parts
|
||||
for (let i = 0; i < len; i++) {
|
||||
const d = (a.nums[i] || 0) - (b.nums[i] || 0);
|
||||
if (d !== 0) return d < 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Handle pre-release comparison
|
||||
if (a.pre && !b.pre) return -1; // prerelease < release
|
||||
if (!a.pre && b.pre) return 1; // release > prerelease
|
||||
if (a.pre === b.pre) return 0; // same or both empty
|
||||
return a.pre < b.pre ? -1 : 1; // basic prerelease tie-break
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for newer version of task-master-ai
|
||||
*/
|
||||
export async function checkForUpdate(
|
||||
currentVersionOverride?: string
|
||||
): Promise<UpdateInfo> {
|
||||
const currentVersion = currentVersionOverride || getCurrentVersion();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
hostname: 'registry.npmjs.org',
|
||||
path: '/task-master-ai',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.npm.install-v1+json',
|
||||
'User-Agent': `task-master-ai/${currentVersion}`
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode !== 200)
|
||||
throw new Error(`npm registry status ${res.statusCode}`);
|
||||
const npmData = JSON.parse(data);
|
||||
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
|
||||
|
||||
const needsUpdate =
|
||||
compareVersions(currentVersion, latestVersion) < 0;
|
||||
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
needsUpdate
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.setTimeout(3000, () => {
|
||||
req.destroy();
|
||||
resolve({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
needsUpdate: false
|
||||
});
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display upgrade notification message
|
||||
*/
|
||||
export function displayUpgradeNotification(
|
||||
currentVersion: string,
|
||||
latestVersion: string
|
||||
) {
|
||||
const message = boxen(
|
||||
`${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` +
|
||||
`Auto-updating to the latest version with new features and bug fixes...`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 1 },
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
);
|
||||
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically update task-master-ai to the latest version
|
||||
*/
|
||||
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).')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const spinner = ora({
|
||||
text: chalk.blue(
|
||||
`Updating task-master-ai to version ${chalk.green(latestVersion)}`
|
||||
),
|
||||
spinner: 'dots',
|
||||
color: 'blue'
|
||||
}).start();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const updateProcess = spawn(
|
||||
'npm',
|
||||
[
|
||||
'install',
|
||||
'-g',
|
||||
`task-master-ai@${latestVersion}`,
|
||||
'--no-fund',
|
||||
'--no-audit',
|
||||
'--loglevel=warn'
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
}
|
||||
);
|
||||
|
||||
let errorOutput = '';
|
||||
|
||||
updateProcess.stdout.on('data', () => {
|
||||
// Update spinner text with progress
|
||||
spinner.text = chalk.blue(
|
||||
`Installing task-master-ai@${latestVersion}...`
|
||||
);
|
||||
});
|
||||
|
||||
updateProcess.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
updateProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
spinner.succeed(
|
||||
chalk.green(
|
||||
`Successfully updated to version ${chalk.bold(latestVersion)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.dim('Please restart your command to use the new version.')
|
||||
);
|
||||
resolve(true);
|
||||
} else {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Please run manually: npm install -g task-master-ai@${latestVersion}`
|
||||
)
|
||||
);
|
||||
if (errorOutput) {
|
||||
console.log(chalk.dim(`Error: ${errorOutput.trim()}`));
|
||||
}
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
updateProcess.on('error', (error) => {
|
||||
spinner.fail(chalk.red('Auto-update failed'));
|
||||
console.log(chalk.red('Error:'), error.message);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Please run manually: npm install -g task-master-ai@${latestVersion}`
|
||||
)
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -50,11 +50,6 @@ export function getStatusWithColor(
|
||||
color: chalk.red,
|
||||
icon: '!',
|
||||
tableIcon: '!'
|
||||
},
|
||||
completed: {
|
||||
color: chalk.green,
|
||||
icon: '✓',
|
||||
tableIcon: '✓'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": false
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
# docs
|
||||
|
||||
## 0.0.4
|
||||
|
||||
## 0.0.3
|
||||
|
||||
## 0.0.2
|
||||
|
||||
## 0.0.1
|
||||
|
||||
@@ -19,7 +19,7 @@ description: "This guide walks you through setting up Task Master in your develo
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package", "task-master-ai", "task-master-mcp"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -48,7 +48,7 @@ description: "Learn how to set up and use Task Master with Cursor AI"
|
||||
<Step title="Configure with the following details:">
|
||||
- Name: "Task Master"
|
||||
- Type: "Command"
|
||||
- Command: "npx -y task-master-ai"
|
||||
- Command: "npx -y --package task-master-ai task-master-mcp"
|
||||
</Step>
|
||||
<Step title="Save Settings">
|
||||
|
||||
|
||||
@@ -83,8 +83,6 @@ Taskmaster uses two primary methods for configuration:
|
||||
- `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
|
||||
- `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'.
|
||||
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI).
|
||||
- **Optional Auto-Update Control:**
|
||||
- `TASKMASTER_SKIP_AUTO_UPDATE`: Set to '1' to disable automatic updates. Also automatically disabled in CI environments (when `CI` environment variable is set).
|
||||
|
||||
**Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables.
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"command": "node",
|
||||
"args": ["./mcp-server/server.js"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -56,7 +56,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
@@ -75,7 +75,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
|
||||
> 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use.
|
||||
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, restart your editor and check that your API keys are correctly configured.
|
||||
> **Note**: If you see `0 tools enabled` in the MCP settings, try removing the `--package=task-master-ai` flag from `args`.
|
||||
|
||||
### VS Code (`servers` + `type`)
|
||||
|
||||
@@ -84,7 +84,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"servers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Task Master documentation powered by Mintlify",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,75 +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
|
||||
|
||||
- Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
|
||||
- task-master-ai@0.27.2
|
||||
|
||||
## 0.25.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e), [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c), [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
|
||||
- task-master-ai@0.27.1
|
||||
|
||||
## 0.25.2-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
|
||||
- task-master-ai@0.27.1-rc.1
|
||||
|
||||
## 0.25.2-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e)]:
|
||||
- task-master-ai@0.27.1-rc.0
|
||||
|
||||
## 0.25.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add "Start Task" button to VS Code extension for seamless Claude Code integration
|
||||
|
||||
You can now click a "Start Task" button directly in the Task Master extension which will open a new terminal and automatically execute the task using Claude Code. This provides a seamless workflow from viewing tasks in the extension to implementing them without leaving VS Code.
|
||||
|
||||
- [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1229](https://github.com/eyaltoledano/claude-task-master/pull/1229) [`674d1f6`](https://github.com/eyaltoledano/claude-task-master/commit/674d1f6de7ea98116b61bdae6198bafe6c4e7c1a) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP not connecting to new Taskmaster version
|
||||
|
||||
- Updated dependencies [[`4e12643`](https://github.com/eyaltoledano/claude-task-master/commit/4e126430a092fb54afb035514fb3d46115714f97), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`a621ff0`](https://github.com/eyaltoledano/claude-task-master/commit/a621ff05eafb51a147a9aabd7b37ddc0e45b0869), [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142)]:
|
||||
- task-master-ai@0.27.0
|
||||
|
||||
## 0.25.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]:
|
||||
- task-master-ai@0.27.0-rc.0
|
||||
|
||||
## 0.24.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
|
||||
- task-master-ai@0.26.0
|
||||
|
||||
## 0.24.2-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -109,7 +109,7 @@ Access settings via **File → Preferences → Settings** and search for "Taskma
|
||||
|
||||
### **MCP Connection Settings**
|
||||
- **MCP Server Command** - Path to task-master-ai executable (default: `npx`)
|
||||
- **MCP Server Args** - Arguments for the server command (default: `-y`, `task-master-ai`)
|
||||
- **MCP Server Args** - Arguments for the server command (default: `-y`, `--package=task-master-ai`, `task-master-ai`)
|
||||
- **Connection Timeout** - Server response timeout (default: 30s)
|
||||
- **Auto Refresh** - Enable automatic task updates (default: enabled)
|
||||
|
||||
|
||||
@@ -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.24.2-rc.1",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
@@ -240,7 +240,7 @@
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"task-master-ai": "0.27.3"
|
||||
"task-master-ai": "0.26.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -254,9 +254,8 @@
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "20.x",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/vscode": "^1.101.0",
|
||||
@@ -272,11 +271,12 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "8.5.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.8.3",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"glob@<8": "^10.4.5",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["-y", "task-master-ai"],
|
||||
"default": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"description": "An array of arguments to pass to the MCP server command."
|
||||
},
|
||||
"taskmaster.mcp.cwd": {
|
||||
|
||||
@@ -11,6 +11,7 @@ interface TaskMetadataSidebarProps {
|
||||
tasks: TaskMasterTask[];
|
||||
complexity: any;
|
||||
isSubtask: boolean;
|
||||
sendMessage: (message: any) => Promise<any>;
|
||||
onStatusChange: (status: TaskMasterTask['status']) => void;
|
||||
onDependencyClick: (depId: string) => void;
|
||||
isRegenerating?: boolean;
|
||||
@@ -22,12 +23,13 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
||||
tasks,
|
||||
complexity,
|
||||
isSubtask,
|
||||
sendMessage,
|
||||
onStatusChange,
|
||||
onDependencyClick,
|
||||
isRegenerating = false,
|
||||
isAppending = false
|
||||
}) => {
|
||||
const { sendMessage } = useVSCodeContext();
|
||||
const { vscode } = useVSCodeContext();
|
||||
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
|
||||
const [mcpComplexityScore, setMcpComplexityScore] = useState<
|
||||
number | undefined
|
||||
@@ -99,37 +101,26 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
||||
};
|
||||
|
||||
// Handle starting a task
|
||||
const handleStartTask = async () => {
|
||||
const handleStartTask = () => {
|
||||
if (!currentTask || isStartingTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsStartingTask(true);
|
||||
|
||||
try {
|
||||
// Send message to extension to open terminal
|
||||
const result = await sendMessage({
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
type: 'openTerminal',
|
||||
data: {
|
||||
taskId: currentTask.id,
|
||||
taskTitle: currentTask.title
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle the response
|
||||
if (result && !result.success) {
|
||||
console.error('Terminal execution failed:', result.error);
|
||||
// The extension will show VS Code error notification and webview toast
|
||||
} else if (result && result.success) {
|
||||
console.log('Terminal started successfully:', result.terminalName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start task:', error);
|
||||
// This handles network/communication errors
|
||||
} finally {
|
||||
// Reset loading state
|
||||
// Reset loading state after a short delay
|
||||
setTimeout(() => {
|
||||
setIsStartingTask(false);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Effect to handle complexity on task change
|
||||
|
||||
@@ -208,6 +208,7 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
|
||||
tasks={allTasks}
|
||||
complexity={complexity}
|
||||
isSubtask={isSubtask}
|
||||
sendMessage={sendMessage}
|
||||
onStatusChange={handleStatusChange}
|
||||
onDependencyClick={handleDependencyClick}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ConfigService } from './services/config-service';
|
||||
import { PollingService } from './services/polling-service';
|
||||
import { createPollingStrategy } from './services/polling-strategies';
|
||||
import { TaskRepository } from './services/task-repository';
|
||||
import { TerminalManager } from './services/terminal-manager';
|
||||
import { WebviewManager } from './services/webview-manager';
|
||||
import { EventEmitter } from './utils/event-emitter';
|
||||
import { ExtensionLogger } from './utils/logger';
|
||||
@@ -23,7 +22,6 @@ let logger: ExtensionLogger;
|
||||
let mcpClient: MCPClientManager;
|
||||
let api: TaskMasterApi;
|
||||
let repository: TaskRepository;
|
||||
let terminalManager: TerminalManager;
|
||||
let pollingService: PollingService;
|
||||
let webviewManager: WebviewManager;
|
||||
let events: EventEmitter;
|
||||
@@ -48,9 +46,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Repository with caching (actually useful for performance)
|
||||
repository = new TaskRepository(api, logger);
|
||||
|
||||
// Terminal manager for task execution
|
||||
terminalManager = new TerminalManager(context, logger);
|
||||
|
||||
// Config service for TaskMaster config.json
|
||||
configService = new ConfigService(logger);
|
||||
|
||||
@@ -61,13 +56,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
pollingService = new PollingService(repository, strategy, logger);
|
||||
|
||||
// Webview manager (cleaner than global panel array) - create before connection
|
||||
webviewManager = new WebviewManager(
|
||||
context,
|
||||
repository,
|
||||
events,
|
||||
logger,
|
||||
terminalManager
|
||||
);
|
||||
webviewManager = new WebviewManager(context, repository, events, logger);
|
||||
webviewManager.setConfigService(configService);
|
||||
|
||||
// Sidebar webview manager
|
||||
@@ -221,11 +210,10 @@ function registerCommands(context: vscode.ExtensionContext) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
export function deactivate() {
|
||||
logger?.log('👋 TaskMaster Extension deactivating...');
|
||||
pollingService?.stop();
|
||||
webviewManager?.dispose();
|
||||
await terminalManager?.dispose();
|
||||
api?.destroy();
|
||||
mcpClient?.disconnect();
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* Terminal Manager - Handles task execution in VS Code terminals
|
||||
* Uses @tm/core for consistent task management with the CLI
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { createTaskMasterCore, type TaskMasterCore } from '@tm/core';
|
||||
import type { ExtensionLogger } from '../utils/logger';
|
||||
|
||||
export interface TerminalExecutionOptions {
|
||||
taskId: string;
|
||||
taskTitle: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface TerminalExecutionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
terminalName?: string;
|
||||
}
|
||||
|
||||
export class TerminalManager {
|
||||
private terminals = new Map<string, vscode.Terminal>();
|
||||
private tmCore?: TaskMasterCore;
|
||||
|
||||
constructor(
|
||||
private context: vscode.ExtensionContext,
|
||||
private logger: ExtensionLogger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute a task in a new VS Code terminal with Claude
|
||||
* Uses @tm/core for consistent task management with the CLI
|
||||
*/
|
||||
async executeTask(
|
||||
options: TerminalExecutionOptions
|
||||
): Promise<TerminalExecutionResult> {
|
||||
const { taskTitle, tag } = options;
|
||||
// Ensure taskId is always a string
|
||||
const taskId = String(options.taskId);
|
||||
|
||||
this.logger.log(
|
||||
`Starting task execution for ${taskId}: ${taskTitle}${tag ? ` (tag: ${tag})` : ''}`
|
||||
);
|
||||
this.logger.log(`TaskId type: ${typeof taskId}, value: ${taskId}`);
|
||||
|
||||
try {
|
||||
// Initialize tm-core if needed
|
||||
await this.initializeCore();
|
||||
|
||||
// Use tm-core to start the task (same as CLI)
|
||||
const startResult = await this.tmCore!.startTask(taskId, {
|
||||
dryRun: false,
|
||||
force: false,
|
||||
updateStatus: true
|
||||
});
|
||||
|
||||
if (!startResult.started || !startResult.executionOutput) {
|
||||
throw new Error(
|
||||
startResult.error || 'Failed to start task with tm-core'
|
||||
);
|
||||
}
|
||||
|
||||
// Create terminal with custom TaskMaster icon
|
||||
const terminalName = `Task ${taskId}: ${taskTitle}`;
|
||||
const terminal = this.createTerminal(terminalName);
|
||||
|
||||
// Store terminal reference for potential cleanup
|
||||
this.terminals.set(taskId, terminal);
|
||||
|
||||
// Show terminal and run Claude command
|
||||
terminal.show();
|
||||
const command = `claude "${startResult.executionOutput}"`;
|
||||
terminal.sendText(command);
|
||||
|
||||
this.logger.log(`Launched Claude for task ${taskId} using tm-core`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
terminalName
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to execute task:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new terminal with TaskMaster branding
|
||||
*/
|
||||
private createTerminal(name: string): vscode.Terminal {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
return vscode.window.createTerminal({
|
||||
name,
|
||||
cwd: workspaceRoot,
|
||||
iconPath: new vscode.ThemeIcon('play') // Use a VS Code built-in icon for now
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMaster Core (same as CLI)
|
||||
*/
|
||||
private async initializeCore(): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) {
|
||||
throw new Error('No workspace folder found');
|
||||
}
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terminal by task ID (if still active)
|
||||
*/
|
||||
getTerminalByTaskId(taskId: string): vscode.Terminal | undefined {
|
||||
return this.terminals.get(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up terminated terminals
|
||||
*/
|
||||
cleanupTerminal(taskId: string): void {
|
||||
const terminal = this.terminals.get(taskId);
|
||||
if (terminal) {
|
||||
this.terminals.delete(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all managed terminals and clean up tm-core
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
this.terminals.forEach((terminal) => {
|
||||
try {
|
||||
terminal.dispose();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to dispose terminal:', error);
|
||||
}
|
||||
});
|
||||
this.terminals.clear();
|
||||
|
||||
if (this.tmCore) {
|
||||
try {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to close tm-core:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import type { EventEmitter } from '../utils/event-emitter';
|
||||
import type { ExtensionLogger } from '../utils/logger';
|
||||
import type { ConfigService } from './config-service';
|
||||
import type { TaskRepository } from './task-repository';
|
||||
import type { TerminalManager } from './terminal-manager';
|
||||
|
||||
export class WebviewManager {
|
||||
private panels = new Set<vscode.WebviewPanel>();
|
||||
@@ -20,8 +19,7 @@ export class WebviewManager {
|
||||
private context: vscode.ExtensionContext,
|
||||
private repository: TaskRepository,
|
||||
private events: EventEmitter,
|
||||
private logger: ExtensionLogger,
|
||||
private terminalManager: TerminalManager
|
||||
private logger: ExtensionLogger
|
||||
) {}
|
||||
|
||||
setConfigService(configService: ConfigService): void {
|
||||
@@ -364,67 +362,27 @@ export class WebviewManager {
|
||||
return;
|
||||
|
||||
case 'openTerminal':
|
||||
// Delegate terminal execution to TerminalManager
|
||||
const { taskId, taskTitle } = data.data || data; // Handle both nested and direct data
|
||||
// Open VS Code terminal for task execution
|
||||
this.logger.log(
|
||||
`Webview openTerminal - taskId: ${taskId} (type: ${typeof taskId}), taskTitle: ${taskTitle}`
|
||||
`Opening terminal for task ${data.taskId}: ${data.taskTitle}`
|
||||
);
|
||||
|
||||
// Get current tag to ensure we're working in the right context
|
||||
let currentTag = 'master'; // default fallback
|
||||
if (this.mcpClient) {
|
||||
try {
|
||||
const tagsResult = await this.mcpClient.callTool('list_tags', {
|
||||
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
|
||||
showMetadata: false
|
||||
const terminal = vscode.window.createTerminal({
|
||||
name: `Task ${data.taskId}: ${data.taskTitle}`,
|
||||
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
});
|
||||
terminal.show();
|
||||
|
||||
let parsedData;
|
||||
if (
|
||||
tagsResult?.content &&
|
||||
Array.isArray(tagsResult.content) &&
|
||||
tagsResult.content[0]?.text
|
||||
) {
|
||||
try {
|
||||
parsedData = JSON.parse(tagsResult.content[0].text);
|
||||
if (parsedData?.data?.currentTag) {
|
||||
currentTag = parsedData.data.currentTag;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
'Failed to parse tags response for terminal execution'
|
||||
);
|
||||
}
|
||||
}
|
||||
this.logger.log('Terminal created and shown successfully');
|
||||
response = { success: true };
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
'Failed to get current tag for terminal execution:',
|
||||
error
|
||||
);
|
||||
this.logger.error('Failed to create terminal:', error);
|
||||
response = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.terminalManager.executeTask({
|
||||
taskId,
|
||||
taskTitle,
|
||||
tag: currentTag
|
||||
});
|
||||
|
||||
response = result;
|
||||
|
||||
// Show user feedback AFTER sending the response (like the working "TaskMaster connected!" example)
|
||||
setImmediate(() => {
|
||||
if (result.success) {
|
||||
// Success: Show info message
|
||||
vscode.window.showInformationMessage(
|
||||
`✅ Started Claude session for Task ${taskId}: ${taskTitle}`
|
||||
);
|
||||
} else {
|
||||
// Error: Show VS Code native error notification only
|
||||
const errorMsg = `Failed to start task: ${result.error}`;
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -408,7 +408,7 @@ export function createMCPConfigFromSettings(): MCPConfig {
|
||||
const taskMasterPath = require.resolve('task-master-ai');
|
||||
const mcpServerPath = path.resolve(
|
||||
path.dirname(taskMasterPath),
|
||||
'./dist/mcp-server.js'
|
||||
'mcp-server/server.js'
|
||||
);
|
||||
|
||||
// Verify the server file exists
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/lib/*": ["./src/lib/*"],
|
||||
"@tm/core": ["../core/src"]
|
||||
"@/lib/*": ["./src/lib/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test", "out", "dist"]
|
||||
|
||||
@@ -451,8 +451,8 @@ When using Task Master in VS Code with MCP support:
|
||||
{
|
||||
"servers": {
|
||||
"task-master-dev": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"command": "node",
|
||||
"args": ["mcp-server/server.js"],
|
||||
"cwd": "/path/to/your/task-master-project",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Available Models as of September 19, 2025
|
||||
# Available Models as of August 12, 2025
|
||||
|
||||
## Main Models
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
| 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 |
|
||||
| grok-cli | grok-4-latest | 0.7 | 0 | 0 |
|
||||
| grok-cli | grok-3-latest | 0.65 | 0 | 0 |
|
||||
| grok-cli | grok-3-fast | 0.6 | 0 | 0 |
|
||||
| grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 |
|
||||
| openai | gpt-4o | 0.332 | 2.5 | 10 |
|
||||
| openai | o1 | 0.489 | 15 | 60 |
|
||||
| openai | o3 | 0.5 | 2 | 8 |
|
||||
@@ -103,10 +99,6 @@
|
||||
| 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 |
|
||||
| grok-cli | grok-4-latest | 0.7 | 0 | 0 |
|
||||
| grok-cli | grok-3-latest | 0.65 | 0 | 0 |
|
||||
| grok-cli | grok-3-fast | 0.6 | 0 | 0 |
|
||||
| grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 |
|
||||
| openai | gpt-4o-search-preview | 0.33 | 2.5 | 10 |
|
||||
| openai | gpt-4o-mini-search-preview | 0.3 | 0.15 | 0.6 |
|
||||
| xai | grok-3 | — | 3 | 15 |
|
||||
@@ -143,10 +135,6 @@
|
||||
| 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 |
|
||||
| grok-cli | grok-4-latest | 0.7 | 0 | 0 |
|
||||
| grok-cli | grok-3-latest | 0.65 | 0 | 0 |
|
||||
| grok-cli | grok-3-fast | 0.6 | 0 | 0 |
|
||||
| grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 |
|
||||
| openai | gpt-4o | 0.332 | 2.5 | 10 |
|
||||
| openai | o3 | 0.5 | 2 | 8 |
|
||||
| openai | o4-mini | 0.45 | 1.1 | 4.4 |
|
||||
|
||||
@@ -23,7 +23,7 @@ npm i -g task-master-ai
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
@@ -145,7 +145,7 @@ You can also set up the MCP server in Cursor settings:
|
||||
4. Configure with the following details:
|
||||
- Name: "Task Master"
|
||||
- Type: "Command"
|
||||
- Command: "npx -y task-master-ai"
|
||||
- Command: "npx -y --package=task-master-ai task-master-ai"
|
||||
5. Save the settings
|
||||
|
||||
Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience.
|
||||
|
||||
@@ -18,17 +18,7 @@ export default {
|
||||
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
|
||||
|
||||
// Transform files
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
||||
transform: {
|
||||
'^.+\\.ts$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true
|
||||
}
|
||||
]
|
||||
},
|
||||
transform: {},
|
||||
|
||||
// Disable transformations for node_modules
|
||||
transformIgnorePatterns: ['/node_modules/'],
|
||||
@@ -37,7 +27,6 @@ export default {
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/$1'
|
||||
},
|
||||
resolver: '<rootDir>/jest.resolver.cjs',
|
||||
|
||||
// Setup module aliases
|
||||
moduleDirectories: ['node_modules', '<rootDir>'],
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const { defaultResolver } = require('jest-resolve');
|
||||
module.exports = function customResolver(request, options) {
|
||||
const resolve = options.defaultResolver || defaultResolver;
|
||||
|
||||
try {
|
||||
return resolve(request, options);
|
||||
} catch (error) {
|
||||
if (request.startsWith('.') && request.endsWith('.js')) {
|
||||
try {
|
||||
return resolve(request.replace(/\.js$/, '.ts'), options);
|
||||
} catch (tsError) {
|
||||
tsError.cause = tsError.cause ?? error;
|
||||
throw tsError;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -17,7 +17,7 @@ Add the following configuration to the user's MCP settings file (`.cursor/mcp.js
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "user_will_add_their_key_here",
|
||||
"PERPLEXITY_API_KEY": "user_will_add_their_key_here",
|
||||
|
||||
3048
package-lock.json
generated
3048
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.27.3",
|
||||
"version": "0.26.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",
|
||||
@@ -11,12 +11,20 @@
|
||||
},
|
||||
"workspaces": ["apps/*", "packages/*", "."],
|
||||
"scripts": {
|
||||
"build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"build": "npm run build:build-config && tsup",
|
||||
"dev": "tsup --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'",
|
||||
"turbo:dev": "turbo dev",
|
||||
"turbo:build": "turbo build",
|
||||
"turbo:typecheck": "turbo typecheck",
|
||||
"dev:main": "tsup --watch --onSuccess 'echo \"📦 Main package built\" && npm link'",
|
||||
"dev:legacy": "npm run build:build-config && concurrently -n \"core,cli,main\" -c \"blue,green,yellow\" \"npm run dev:core\" \"npm run dev:cli\" \"npm run dev:main\"",
|
||||
"dev:core": "npm run dev -w @tm/core",
|
||||
"dev:cli": "npm run dev -w @tm/cli",
|
||||
"build:packages": "turbo build --filter='./packages/*' --filter='./apps/*'",
|
||||
"build:packages:parallel": "turbo build --filter='./packages/*' --filter='./apps/*'",
|
||||
"build:build-config": "npm run build -w @tm/build-config",
|
||||
"build:core": "npm run build -w @tm/core",
|
||||
"build:cli": "npm run build -w @tm/cli",
|
||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
|
||||
"test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration",
|
||||
@@ -33,9 +41,7 @@
|
||||
"inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js",
|
||||
"mcp-server": "node dist/mcp-server.js",
|
||||
"format-check": "biome format .",
|
||||
"format": "biome format . --write",
|
||||
"deps:check": "manypkg check || echo 'Note: Workspace package version warnings are expected for internal @tm/* packages'",
|
||||
"deps:fix": "manypkg fix"
|
||||
"format": "biome format . --write"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -67,7 +73,7 @@
|
||||
"@inquirer/search": "^3.0.15",
|
||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"@tm/cli": "*",
|
||||
"ai": "^4.3.10",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
@@ -76,7 +82,7 @@
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-progress": "^3.12.0",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^12.1.0",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.21.2",
|
||||
@@ -126,8 +132,6 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.28.1",
|
||||
"@manypkg/cli": "^0.25.1",
|
||||
"@tm/cli": "*",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -139,10 +143,9 @@
|
||||
"mock-fs": "^5.5.0",
|
||||
"prettier": "^3.5.3",
|
||||
"supertest": "^7.1.0",
|
||||
"ts-jest": "^29.4.2",
|
||||
"tsdown": "^0.15.2",
|
||||
"tsx": "^4.20.4",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.16.2",
|
||||
"turbo": "^2.5.6",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# @tm/build-config
|
||||
|
||||
## null
|
||||
|
||||
## 1.0.1
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@tm/build-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared build configuration for Task Master monorepo",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./dist/tsdown.base.js",
|
||||
"types": "./src/tsdown.base.ts",
|
||||
"main": "./dist/tsup.base.js",
|
||||
"types": "./dist/tsup.base.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/tsdown.base.ts",
|
||||
"import": "./dist/tsdown.base.js"
|
||||
"types": "./dist/tsup.base.d.ts",
|
||||
"import": "./dist/tsup.base.js"
|
||||
}
|
||||
},
|
||||
"files": ["dist", "src"],
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Base tsdown configuration for Task Master monorepo
|
||||
* Provides shared configuration that can be extended by individual packages
|
||||
*/
|
||||
import type { UserConfig } from 'tsdown';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const isDevelopment = !isProduction;
|
||||
|
||||
/**
|
||||
* Environment helpers
|
||||
*/
|
||||
export const env = {
|
||||
isProduction,
|
||||
isDevelopment,
|
||||
NODE_ENV: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
|
||||
/**
|
||||
* Base tsdown configuration for all packages
|
||||
* Since everything gets bundled into root dist/ anyway, use consistent settings
|
||||
*/
|
||||
export const baseConfig: Partial<UserConfig> = {
|
||||
sourcemap: isDevelopment,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
dts: isDevelopment,
|
||||
minify: isProduction,
|
||||
treeshake: isProduction,
|
||||
// Better debugging in development
|
||||
...(isDevelopment && {
|
||||
keepNames: true,
|
||||
splitting: false // Disable code splitting for better stack traces
|
||||
}),
|
||||
// Keep all npm dependencies external (available via node_modules)
|
||||
external: [/^[^@./]/, /^@(?!tm\/)/]
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to merge configurations
|
||||
* Simplified for tsdown usage
|
||||
*/
|
||||
export function mergeConfig(
|
||||
base: Partial<UserConfig>,
|
||||
overrides: Partial<UserConfig>
|
||||
): Partial<UserConfig> {
|
||||
return {
|
||||
...base,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
101
packages/build-config/src/tsup.base.ts
Normal file
101
packages/build-config/src/tsup.base.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Base tsup configuration for Task Master monorepo
|
||||
* Provides shared configuration that can be extended by individual packages
|
||||
*/
|
||||
import type { Options } from 'tsup';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const isDevelopment = !isProduction;
|
||||
|
||||
/**
|
||||
* Environment helpers
|
||||
*/
|
||||
export const env = {
|
||||
isProduction,
|
||||
isDevelopment,
|
||||
NODE_ENV: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
|
||||
/**
|
||||
* Base tsup configuration for all packages
|
||||
* Since everything gets bundled into root dist/ anyway, use consistent settings
|
||||
*/
|
||||
export const baseConfig: Partial<Options> = {
|
||||
format: ['esm'],
|
||||
target: 'node18',
|
||||
sourcemap: isDevelopment,
|
||||
clean: true,
|
||||
dts: false,
|
||||
minify: isProduction,
|
||||
treeshake: isProduction,
|
||||
splitting: false,
|
||||
// Don't bundle any other dependencies (auto-external all node_modules)
|
||||
external: [/^[^./]/],
|
||||
esbuildOptions(options) {
|
||||
options.platform = 'node';
|
||||
// Allow importing TypeScript from JavaScript
|
||||
options.resolveExtensions = ['.ts', '.js', '.mjs', '.json'];
|
||||
// Better source mapping in development only
|
||||
options.sourcesContent = isDevelopment;
|
||||
// Keep original names for better debugging in development
|
||||
options.keepNames = isDevelopment;
|
||||
},
|
||||
// Watch mode configuration for development
|
||||
watch: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Legacy external modules list - kept for backwards compatibility
|
||||
* Note: When using tsup-node, this is not needed as it automatically
|
||||
* excludes dependencies and peerDependencies from package.json
|
||||
*/
|
||||
export const commonExternals = [
|
||||
// Native Node.js modules (for cases where tsup is used instead of tsup-node)
|
||||
'fs',
|
||||
'path',
|
||||
'child_process',
|
||||
'crypto',
|
||||
'os',
|
||||
'url',
|
||||
'util',
|
||||
'stream',
|
||||
'http',
|
||||
'https',
|
||||
'events',
|
||||
'assert',
|
||||
'buffer',
|
||||
'querystring',
|
||||
'readline',
|
||||
'zlib',
|
||||
'tty',
|
||||
'net',
|
||||
'dgram',
|
||||
'dns',
|
||||
'tls',
|
||||
'cluster',
|
||||
'process',
|
||||
'module'
|
||||
];
|
||||
|
||||
/**
|
||||
* Utility function to merge configurations
|
||||
* Simplified for tsup-node usage
|
||||
*/
|
||||
export function mergeConfig(
|
||||
baseConfig: Partial<Options>,
|
||||
overrides: Partial<Options>
|
||||
): Options {
|
||||
return {
|
||||
...baseConfig,
|
||||
...overrides,
|
||||
// Merge esbuildOptions
|
||||
esbuildOptions(options, context) {
|
||||
if (baseConfig.esbuildOptions) {
|
||||
baseConfig.esbuildOptions(options, context);
|
||||
}
|
||||
if (overrides.esbuildOptions) {
|
||||
overrides.esbuildOptions(options, context);
|
||||
}
|
||||
}
|
||||
} as Options;
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## null
|
||||
|
||||
## 0.26.1
|
||||
|
||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
@@ -12,7 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Initial package structure and configuration
|
||||
- TypeScript support with strict mode
|
||||
- Dual ESM/CJS build system with tsup
|
||||
@@ -23,7 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Comprehensive documentation and README
|
||||
|
||||
### Development Infrastructure
|
||||
|
||||
- tsup configuration for dual format builds
|
||||
- Jest configuration with ESM support
|
||||
- ESLint configuration with TypeScript rules
|
||||
@@ -33,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- .gitignore for development files
|
||||
|
||||
### Package Structure
|
||||
|
||||
- `src/types/` - TypeScript type definitions (placeholder)
|
||||
- `src/providers/` - AI provider implementations (placeholder)
|
||||
- `src/storage/` - Storage layer abstractions (placeholder)
|
||||
@@ -45,7 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [1.0.0] - TBD
|
||||
|
||||
### Planned Features
|
||||
|
||||
- Complete TypeScript type system
|
||||
- AI provider implementations
|
||||
- Storage adapters
|
||||
@@ -60,11 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## Release Notes
|
||||
|
||||
### Version 1.0.0 (Coming Soon)
|
||||
|
||||
This will be the first stable release of tm-core with complete implementations of all modules. Currently, all modules contain placeholder implementations to establish the package structure and enable development of dependent packages.
|
||||
|
||||
### Development Status
|
||||
|
||||
- ✅ Package structure and configuration
|
||||
- ✅ Build and test infrastructure
|
||||
- ✅ Development tooling setup
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@tm/core",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Core library for Task Master - TypeScript task management system",
|
||||
"type": "module",
|
||||
@@ -31,18 +32,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@tm/build-config": "*",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^20.11.30",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"dotenv-mono": "^1.3.14",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^2.1.8"
|
||||
"typescript": "^5.4.3",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { ConfigManager } from './config-manager.js';
|
||||
import { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from './services/config-loader.service.js';
|
||||
import { ConfigMerger } from './services/config-merger.service.js';
|
||||
import { RuntimeStateManager } from './services/runtime-state-manager.service.js';
|
||||
@@ -70,8 +69,8 @@ describe('ConfigManager', () => {
|
||||
({
|
||||
loadState: vi.fn().mockResolvedValue({ activeTag: 'master' }),
|
||||
saveState: vi.fn().mockResolvedValue(undefined),
|
||||
getCurrentTag: vi.fn().mockReturnValue('master'),
|
||||
setCurrentTag: vi.fn().mockResolvedValue(undefined),
|
||||
getActiveTag: vi.fn().mockReturnValue('master'),
|
||||
setActiveTag: vi.fn().mockResolvedValue(undefined),
|
||||
getState: vi.fn().mockReturnValue({ activeTag: 'master' }),
|
||||
updateMetadata: vi.fn().mockResolvedValue(undefined),
|
||||
clearState: vi.fn().mockResolvedValue(undefined)
|
||||
@@ -228,8 +227,8 @@ describe('ConfigManager', () => {
|
||||
|
||||
const models = manager.getModelConfig();
|
||||
expect(models).toEqual({
|
||||
main: DEFAULT_CONFIG_VALUES.MODELS.MAIN,
|
||||
fallback: DEFAULT_CONFIG_VALUES.MODELS.FALLBACK
|
||||
main: 'claude-3-5-sonnet-20241022',
|
||||
fallback: 'gpt-4o-mini'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,7 +281,7 @@ describe('ConfigManager', () => {
|
||||
await manager.setActiveTag('feature-branch');
|
||||
|
||||
const stateManager = (manager as any).stateManager;
|
||||
expect(stateManager.setCurrentTag).toHaveBeenCalledWith('feature-branch');
|
||||
expect(stateManager.setActiveTag).toHaveBeenCalledWith('feature-branch');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
PartialConfiguration,
|
||||
RuntimeStorageConfig
|
||||
} from '../interfaces/configuration.interface.js';
|
||||
import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../interfaces/configuration.interface.js';
|
||||
import { ConfigLoader } from './services/config-loader.service.js';
|
||||
import {
|
||||
ConfigMerger,
|
||||
@@ -168,8 +167,8 @@ export class ConfigManager {
|
||||
getModelConfig() {
|
||||
return (
|
||||
this.config.models || {
|
||||
main: DEFAULTS.MODELS.MAIN,
|
||||
fallback: DEFAULTS.MODELS.FALLBACK
|
||||
main: 'claude-3-5-sonnet-20241022',
|
||||
fallback: 'gpt-4o-mini'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
||||
*/
|
||||
export const STATUS_ICONS: Record<TaskStatus, string> = {
|
||||
done: '✓',
|
||||
completed: '✓',
|
||||
'in-progress': '►',
|
||||
blocked: '⭕',
|
||||
pending: '○',
|
||||
@@ -72,6 +71,5 @@ export const STATUS_COLORS: Record<TaskStatus, string> = {
|
||||
deferred: 'gray',
|
||||
cancelled: 'red',
|
||||
blocked: 'magenta',
|
||||
review: 'cyan',
|
||||
completed: 'green'
|
||||
review: 'cyan'
|
||||
} as const;
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Base executor class providing common functionality for all executors
|
||||
*/
|
||||
|
||||
import type { Task } from '../types/index.js';
|
||||
import type { ITaskExecutor, ExecutorType, ExecutionResult } from './types.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
|
||||
export abstract class BaseExecutor implements ITaskExecutor {
|
||||
protected readonly logger = getLogger('BaseExecutor');
|
||||
protected readonly projectRoot: string;
|
||||
protected readonly config: Record<string, any>;
|
||||
|
||||
constructor(projectRoot: string, config: Record<string, any> = {}) {
|
||||
this.projectRoot = projectRoot;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
abstract execute(task: Task): Promise<ExecutionResult>;
|
||||
abstract getType(): ExecutorType;
|
||||
abstract isAvailable(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Format task details into a readable prompt
|
||||
*/
|
||||
protected formatTaskPrompt(task: Task): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
sections.push(`Task ID: ${task.id}`);
|
||||
sections.push(`Title: ${task.title}`);
|
||||
|
||||
if (task.description) {
|
||||
sections.push(`\nDescription:\n${task.description}`);
|
||||
}
|
||||
|
||||
if (task.details) {
|
||||
sections.push(`\nImplementation Details:\n${task.details}`);
|
||||
}
|
||||
|
||||
if (task.testStrategy) {
|
||||
sections.push(`\nTest Strategy:\n${task.testStrategy}`);
|
||||
}
|
||||
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
sections.push(`\nDependencies: ${task.dependencies.join(', ')}`);
|
||||
}
|
||||
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
const subtaskList = task.subtasks
|
||||
.map((st) => ` - [${st.status}] ${st.id}: ${st.title}`)
|
||||
.join('\n');
|
||||
sections.push(`\nSubtasks:\n${subtaskList}`);
|
||||
}
|
||||
|
||||
sections.push(`\nStatus: ${task.status}`);
|
||||
sections.push(`Priority: ${task.priority}`);
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create base execution result
|
||||
*/
|
||||
protected createResult(
|
||||
taskId: string,
|
||||
success: boolean,
|
||||
output?: string,
|
||||
error?: string
|
||||
): ExecutionResult {
|
||||
return {
|
||||
success,
|
||||
taskId,
|
||||
executorType: this.getType(),
|
||||
output,
|
||||
error,
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/**
|
||||
* Claude executor implementation for Task Master
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import type { Task } from '../types/index.js';
|
||||
import type {
|
||||
ExecutorType,
|
||||
ExecutionResult,
|
||||
ClaudeExecutorConfig
|
||||
} from './types.js';
|
||||
import { BaseExecutor } from './base-executor.js';
|
||||
|
||||
export class ClaudeExecutor extends BaseExecutor {
|
||||
private claudeConfig: ClaudeExecutorConfig;
|
||||
private currentProcess: any = null;
|
||||
|
||||
constructor(projectRoot: string, config: ClaudeExecutorConfig = {}) {
|
||||
super(projectRoot, config);
|
||||
this.claudeConfig = {
|
||||
command: config.command || 'claude',
|
||||
systemPrompt:
|
||||
config.systemPrompt ||
|
||||
'You are a helpful AI assistant helping to complete a software development task.',
|
||||
additionalFlags: config.additionalFlags || []
|
||||
};
|
||||
}
|
||||
|
||||
getType(): ExecutorType {
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const checkProcess = spawn('which', [this.claudeConfig.command!], {
|
||||
shell: true
|
||||
});
|
||||
|
||||
checkProcess.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
checkProcess.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async execute(task: Task): Promise<ExecutionResult> {
|
||||
const startTime = new Date().toISOString();
|
||||
|
||||
try {
|
||||
// Check if Claude is available
|
||||
const isAvailable = await this.isAvailable();
|
||||
if (!isAvailable) {
|
||||
return this.createResult(
|
||||
task.id,
|
||||
false,
|
||||
undefined,
|
||||
`Claude CLI not found. Please ensure 'claude' command is available in PATH.`
|
||||
);
|
||||
}
|
||||
|
||||
// Format the task into a prompt
|
||||
const taskPrompt = this.formatTaskPrompt(task);
|
||||
const fullPrompt = `${this.claudeConfig.systemPrompt}\n\nHere is the task to complete:\n\n${taskPrompt}`;
|
||||
|
||||
// Execute Claude with the task details
|
||||
const result = await this.runClaude(fullPrompt, task.id);
|
||||
|
||||
return {
|
||||
...result,
|
||||
startTime,
|
||||
endTime: new Date().toISOString()
|
||||
};
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to execute task ${task.id}:`, error);
|
||||
return this.createResult(
|
||||
task.id,
|
||||
false,
|
||||
undefined,
|
||||
error.message || 'Unknown error occurred'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private runClaude(prompt: string, taskId: string): Promise<ExecutionResult> {
|
||||
return new Promise((resolve) => {
|
||||
const args = [prompt, ...this.claudeConfig.additionalFlags!];
|
||||
|
||||
this.logger.info(`Executing Claude for task ${taskId}`);
|
||||
this.logger.debug(
|
||||
`Command: ${this.claudeConfig.command} ${args.join(' ')}`
|
||||
);
|
||||
|
||||
this.currentProcess = spawn(this.claudeConfig.command!, args, {
|
||||
cwd: this.projectRoot,
|
||||
shell: false,
|
||||
stdio: 'inherit' // Let Claude handle its own I/O
|
||||
});
|
||||
|
||||
this.currentProcess.on('close', (code: number) => {
|
||||
this.currentProcess = null;
|
||||
|
||||
if (code === 0) {
|
||||
resolve(
|
||||
this.createResult(
|
||||
taskId,
|
||||
true,
|
||||
'Claude session completed successfully'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
resolve(
|
||||
this.createResult(
|
||||
taskId,
|
||||
false,
|
||||
undefined,
|
||||
`Claude exited with code ${code}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.currentProcess.on('error', (error: any) => {
|
||||
this.currentProcess = null;
|
||||
this.logger.error(`Claude process error:`, error);
|
||||
resolve(
|
||||
this.createResult(
|
||||
taskId,
|
||||
false,
|
||||
undefined,
|
||||
`Failed to spawn Claude: ${error.message}`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.currentProcess) {
|
||||
this.logger.info('Stopping Claude process...');
|
||||
this.currentProcess.kill('SIGTERM');
|
||||
this.currentProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Factory for creating task executors
|
||||
*/
|
||||
|
||||
import type { ITaskExecutor, ExecutorOptions, ExecutorType } from './types.js';
|
||||
import { ClaudeExecutor } from './claude-executor.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
|
||||
export class ExecutorFactory {
|
||||
private static logger = getLogger('ExecutorFactory');
|
||||
|
||||
/**
|
||||
* Create an executor based on the provided options
|
||||
*/
|
||||
static create(options: ExecutorOptions): ITaskExecutor {
|
||||
this.logger.debug(`Creating executor of type: ${options.type}`);
|
||||
|
||||
switch (options.type) {
|
||||
case 'claude':
|
||||
return new ClaudeExecutor(options.projectRoot, options.config);
|
||||
|
||||
case 'shell':
|
||||
// Placeholder for shell executor
|
||||
throw new Error('Shell executor not yet implemented');
|
||||
|
||||
case 'custom':
|
||||
// Placeholder for custom executor
|
||||
throw new Error('Custom executor not yet implemented');
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown executor type: ${options.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default executor type based on available tools
|
||||
*/
|
||||
static async getDefaultExecutor(
|
||||
projectRoot: string
|
||||
): Promise<ExecutorType | null> {
|
||||
// Check for Claude first
|
||||
const claudeExecutor = new ClaudeExecutor(projectRoot);
|
||||
if (await claudeExecutor.isAvailable()) {
|
||||
this.logger.info('Claude CLI detected as default executor');
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
// Could check for other executors here
|
||||
this.logger.warn('No default executor available');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available executor types
|
||||
*/
|
||||
static getAvailableTypes(): ExecutorType[] {
|
||||
return ['claude', 'shell', 'custom'];
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Service for managing task execution
|
||||
*/
|
||||
|
||||
import type { Task } from '../types/index.js';
|
||||
import type {
|
||||
ITaskExecutor,
|
||||
ExecutorOptions,
|
||||
ExecutionResult,
|
||||
ExecutorType
|
||||
} from './types.js';
|
||||
import { ExecutorFactory } from './executor-factory.js';
|
||||
import { getLogger } from '../logger/index.js';
|
||||
|
||||
export interface ExecutorServiceOptions {
|
||||
projectRoot: string;
|
||||
defaultExecutor?: ExecutorType;
|
||||
executorConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class ExecutorService {
|
||||
private logger = getLogger('ExecutorService');
|
||||
private projectRoot: string;
|
||||
private defaultExecutor?: ExecutorType;
|
||||
private executorConfig: Record<string, any>;
|
||||
private currentExecutor?: ITaskExecutor;
|
||||
|
||||
constructor(options: ExecutorServiceOptions) {
|
||||
this.projectRoot = options.projectRoot;
|
||||
this.defaultExecutor = options.defaultExecutor;
|
||||
this.executorConfig = options.executorConfig || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task
|
||||
*/
|
||||
async executeTask(
|
||||
task: Task,
|
||||
executorType?: ExecutorType
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
// Determine executor type
|
||||
const type =
|
||||
executorType ||
|
||||
this.defaultExecutor ||
|
||||
(await ExecutorFactory.getDefaultExecutor(this.projectRoot));
|
||||
if (!type) {
|
||||
return {
|
||||
success: false,
|
||||
taskId: task.id,
|
||||
executorType: 'claude',
|
||||
error:
|
||||
'No executor available. Please install Claude CLI or specify an executor type.',
|
||||
startTime: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Create executor
|
||||
const executorOptions: ExecutorOptions = {
|
||||
type,
|
||||
projectRoot: this.projectRoot,
|
||||
config: this.executorConfig
|
||||
};
|
||||
|
||||
this.currentExecutor = ExecutorFactory.create(executorOptions);
|
||||
|
||||
// Check if executor is available
|
||||
const isAvailable = await this.currentExecutor.isAvailable();
|
||||
if (!isAvailable) {
|
||||
return {
|
||||
success: false,
|
||||
taskId: task.id,
|
||||
executorType: type,
|
||||
error: `Executor ${type} is not available or not configured properly`,
|
||||
startTime: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Execute the task
|
||||
this.logger.info(`Starting task ${task.id} with ${type} executor`);
|
||||
const result = await this.currentExecutor.execute(task);
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to execute task ${task.id}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
taskId: task.id,
|
||||
executorType: executorType || 'claude',
|
||||
error: error.message || 'Unknown error occurred',
|
||||
startTime: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current task execution
|
||||
*/
|
||||
async stopCurrentTask(): Promise<void> {
|
||||
if (this.currentExecutor && this.currentExecutor.stop) {
|
||||
await this.currentExecutor.stop();
|
||||
this.currentExecutor = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Public API for the executors module
|
||||
*/
|
||||
|
||||
export * from './types.js';
|
||||
export { BaseExecutor } from './base-executor.js';
|
||||
export { ClaudeExecutor } from './claude-executor.js';
|
||||
export { ExecutorFactory } from './executor-factory.js';
|
||||
export {
|
||||
ExecutorService,
|
||||
type ExecutorServiceOptions
|
||||
} from './executor-service.js';
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Executor types and interfaces for Task Master
|
||||
*/
|
||||
|
||||
import type { Task } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Supported executor types
|
||||
*/
|
||||
export type ExecutorType = 'claude' | 'shell' | 'custom';
|
||||
|
||||
/**
|
||||
* Options for executor creation
|
||||
*/
|
||||
export interface ExecutorOptions {
|
||||
type: ExecutorType;
|
||||
projectRoot: string;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from task execution
|
||||
*/
|
||||
export interface ExecutionResult {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
executorType: ExecutorType;
|
||||
output?: string;
|
||||
error?: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
processId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for all task executors
|
||||
*/
|
||||
export interface ITaskExecutor {
|
||||
/**
|
||||
* Execute a task
|
||||
*/
|
||||
execute(task: Task): Promise<ExecutionResult>;
|
||||
|
||||
/**
|
||||
* Stop a running task execution
|
||||
*/
|
||||
stop?(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get executor type
|
||||
*/
|
||||
getType(): ExecutorType;
|
||||
|
||||
/**
|
||||
* Check if executor is available/configured
|
||||
*/
|
||||
isAvailable(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Claude executor
|
||||
*/
|
||||
export interface ClaudeExecutorConfig {
|
||||
command?: string; // Default: 'claude'
|
||||
systemPrompt?: string;
|
||||
additionalFlags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Shell executor
|
||||
*/
|
||||
export interface ShellExecutorConfig {
|
||||
shell?: string; // Default: '/bin/bash'
|
||||
env?: Record<string, string>;
|
||||
cwd?: string;
|
||||
}
|
||||
@@ -8,10 +8,7 @@ export {
|
||||
TaskMasterCore,
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCoreOptions,
|
||||
type ListTasksResult,
|
||||
type StartTaskOptions,
|
||||
type StartTaskResult,
|
||||
type ConflictCheckResult
|
||||
type ListTasksResult
|
||||
} from './task-master-core.js';
|
||||
|
||||
// Re-export types
|
||||
@@ -58,6 +55,3 @@ export {
|
||||
|
||||
// Re-export logger
|
||||
export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
|
||||
|
||||
// Re-export executors
|
||||
export * from './executors/index.js';
|
||||
|
||||
@@ -399,8 +399,8 @@ export interface IConfigurationManager {
|
||||
*/
|
||||
export const DEFAULT_CONFIG_VALUES = {
|
||||
MODELS: {
|
||||
MAIN: 'claude-sonnet-4-20250514',
|
||||
FALLBACK: 'claude-3-7-sonnet-20250219'
|
||||
MAIN: 'claude-3-5-sonnet-20241022',
|
||||
FALLBACK: 'gpt-4o-mini'
|
||||
},
|
||||
TASKS: {
|
||||
DEFAULT_PRIORITY: 'medium' as TaskPriority,
|
||||
|
||||
@@ -3,17 +3,7 @@
|
||||
* This file defines the contract for all storage implementations
|
||||
*/
|
||||
|
||||
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Result type for updateTaskStatus operations
|
||||
*/
|
||||
export interface UpdateStatusResult {
|
||||
success: boolean;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
taskId: string;
|
||||
}
|
||||
import type { Task, TaskMetadata } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Interface for storage operations on tasks
|
||||
@@ -27,14 +17,6 @@ export interface IStorage {
|
||||
*/
|
||||
loadTasks(tag?: string): Promise<Task[]>;
|
||||
|
||||
/**
|
||||
* Load a single task by ID
|
||||
* @param taskId - ID of the task to load
|
||||
* @param tag - Optional tag context for the task
|
||||
* @returns Promise that resolves to the task or null if not found
|
||||
*/
|
||||
loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||
|
||||
/**
|
||||
* Save tasks to storage, replacing existing tasks
|
||||
* @param tasks - Array of tasks to save
|
||||
@@ -64,19 +46,6 @@ export interface IStorage {
|
||||
tag?: string
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID
|
||||
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
|
||||
* @param newStatus - New status to set
|
||||
* @param tag - Optional tag context for the task
|
||||
* @returns Promise that resolves to update result with old and new status
|
||||
*/
|
||||
updateTaskStatus(
|
||||
taskId: string,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<UpdateStatusResult>;
|
||||
|
||||
/**
|
||||
* Delete a task by ID
|
||||
* @param taskId - ID of the task to delete
|
||||
@@ -206,7 +175,6 @@ export abstract class BaseStorage implements IStorage {
|
||||
|
||||
// Abstract methods that must be implemented by concrete classes
|
||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||
abstract updateTask(
|
||||
@@ -214,11 +182,6 @@ export abstract class BaseStorage implements IStorage {
|
||||
updates: Partial<Task>,
|
||||
tag?: string
|
||||
): Promise<void>;
|
||||
abstract updateTaskStatus(
|
||||
taskId: string,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<UpdateStatusResult>;
|
||||
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
|
||||
abstract exists(tag?: string): Promise<boolean>;
|
||||
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
|
||||
|
||||
@@ -127,7 +127,7 @@ export class TaskMapper {
|
||||
/**
|
||||
* Maps database status to internal status
|
||||
*/
|
||||
static mapStatus(
|
||||
private static mapStatus(
|
||||
status: Database['public']['Enums']['task_status']
|
||||
): Task['status'] {
|
||||
switch (status) {
|
||||
|
||||
@@ -26,7 +26,7 @@ const MAX_PROMPT_LENGTH = 100000;
|
||||
const MIN_TEMPERATURE = 0;
|
||||
const MAX_TEMPERATURE = 2;
|
||||
const MIN_MAX_TOKENS = 1;
|
||||
const MAX_MAX_TOKENS = 131072;
|
||||
const MAX_MAX_TOKENS = 100000;
|
||||
|
||||
/**
|
||||
* Configuration for BaseProvider
|
||||
|
||||
@@ -3,30 +3,6 @@ import { Task } from '../types/index.js';
|
||||
import { Database } from '../types/database.types.js';
|
||||
import { TaskMapper } from '../mappers/TaskMapper.js';
|
||||
import { AuthManager } from '../auth/auth-manager.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Zod schema for task status validation
|
||||
const TaskStatusSchema = z.enum([
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'review',
|
||||
'deferred',
|
||||
'cancelled',
|
||||
'blocked'
|
||||
]);
|
||||
|
||||
// Zod schema for task updates
|
||||
const TaskUpdateSchema = z
|
||||
.object({
|
||||
title: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
status: TaskStatusSchema.optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||
details: z.string().optional(),
|
||||
testStrategy: z.string().optional()
|
||||
})
|
||||
.partial();
|
||||
|
||||
export class SupabaseTaskRepository {
|
||||
constructor(private supabase: SupabaseClient<Database>) {}
|
||||
@@ -84,22 +60,12 @@ export class SupabaseTaskRepository {
|
||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||
}
|
||||
|
||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||
// Get the current context to determine briefId (projectId not used in Supabase context)
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
async getTask(accountId: string, taskId: string): Promise<Task | null> {
|
||||
const { data, error } = await this.supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.eq('brief_id', context.briefId)
|
||||
.eq('display_id', taskId.toUpperCase())
|
||||
.eq('account_id', accountId)
|
||||
.eq('id', taskId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
@@ -141,84 +107,4 @@ export class SupabaseTaskRepository {
|
||||
dependenciesByTaskId
|
||||
);
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
updates: Partial<Task>
|
||||
): Promise<Task> {
|
||||
// Get the current context to determine briefId
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context || !context.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief'
|
||||
);
|
||||
}
|
||||
|
||||
// Validate updates using Zod schema
|
||||
try {
|
||||
TaskUpdateSchema.parse(updates);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errorMessages = error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Convert Task fields to database fields - only include fields that actually exist in the database
|
||||
const dbUpdates: any = {};
|
||||
|
||||
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||
if (updates.description !== undefined)
|
||||
dbUpdates.description = updates.description;
|
||||
if (updates.status !== undefined)
|
||||
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
|
||||
// Skip fields that don't exist in database schema: details, testStrategy, etc.
|
||||
|
||||
// Update the task
|
||||
const { error } = await this.supabase
|
||||
.from('tasks')
|
||||
.update(dbUpdates)
|
||||
.eq('brief_id', context.briefId)
|
||||
.eq('display_id', taskId.toUpperCase());
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to update task: ${error.message}`);
|
||||
}
|
||||
|
||||
// Return the updated task by fetching it
|
||||
const updatedTask = await this.getTask(projectId, taskId);
|
||||
if (!updatedTask) {
|
||||
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
||||
}
|
||||
|
||||
return updatedTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps internal status to database status
|
||||
*/
|
||||
private mapStatusToDatabase(
|
||||
status: string
|
||||
): Database['public']['Enums']['task_status'] {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'todo';
|
||||
case 'in-progress':
|
||||
case 'in_progress': // Accept both formats
|
||||
return 'in_progress';
|
||||
case 'done':
|
||||
return 'done';
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
/**
|
||||
* @fileoverview TaskExecutionService for handling task execution business logic
|
||||
* Extracted from CLI start command to be reusable across CLI and extension
|
||||
*/
|
||||
|
||||
import type { Task } from '../types/index.js';
|
||||
import type { TaskService } from './task-service.js';
|
||||
|
||||
export interface StartTaskOptions {
|
||||
subtaskId?: string;
|
||||
dryRun?: boolean;
|
||||
updateStatus?: boolean;
|
||||
force?: boolean;
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
export interface StartTaskResult {
|
||||
task: Task | null;
|
||||
found: boolean;
|
||||
started: boolean;
|
||||
subtaskId?: string;
|
||||
subtask?: any;
|
||||
error?: string;
|
||||
executionOutput?: string;
|
||||
/** Command to execute (for CLI to run directly) */
|
||||
command?: {
|
||||
executable: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConflictCheckResult {
|
||||
canProceed: boolean;
|
||||
conflictingTasks: Task[];
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskExecutionService handles the business logic for starting and executing tasks
|
||||
*/
|
||||
export class TaskExecutionService {
|
||||
constructor(private taskService: TaskService) {}
|
||||
|
||||
/**
|
||||
* Start working on a task with comprehensive business logic
|
||||
*/
|
||||
async startTask(
|
||||
taskId: string,
|
||||
options: StartTaskOptions = {}
|
||||
): Promise<StartTaskResult> {
|
||||
try {
|
||||
// Handle subtask IDs by extracting parent task ID
|
||||
const { parentId, subtaskId } = this.parseTaskId(taskId);
|
||||
|
||||
// Check for in-progress task conflicts
|
||||
if (!options.force) {
|
||||
const conflictCheck = await this.checkInProgressConflicts(taskId);
|
||||
if (!conflictCheck.canProceed) {
|
||||
return {
|
||||
task: null,
|
||||
found: false,
|
||||
started: false,
|
||||
error: `Conflicting tasks in progress: ${conflictCheck.conflictingTasks.map((t) => `#${t.id}: ${t.title}`).join(', ')}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actual task (parent task if dealing with subtask)
|
||||
const task = await this.taskService.getTask(parentId);
|
||||
if (!task) {
|
||||
return {
|
||||
task: null,
|
||||
found: false,
|
||||
started: false,
|
||||
error: `Task ${parentId} not found`
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific subtask if provided
|
||||
let subtask = undefined;
|
||||
if (subtaskId && task.subtasks) {
|
||||
subtask = task.subtasks.find((st) => String(st.id) === subtaskId);
|
||||
}
|
||||
|
||||
// Update task status to in-progress if not disabled
|
||||
if (options.updateStatus && !options.dryRun) {
|
||||
try {
|
||||
await this.taskService.updateTaskStatus(parentId, 'in-progress');
|
||||
} catch (error) {
|
||||
// Log but don't fail - status update is not critical
|
||||
console.warn(
|
||||
`Could not update task status: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare execution command instead of executing directly
|
||||
let started = false;
|
||||
let executionOutput = 'Task ready to execute';
|
||||
let command = undefined;
|
||||
|
||||
if (!options.dryRun) {
|
||||
// Prepare the command for execution by the CLI
|
||||
command = await this.prepareExecutionCommand(task, subtask);
|
||||
started = !!command; // Command prepared successfully
|
||||
executionOutput = command
|
||||
? `Command prepared: ${command.executable} ${command.args.join(' ')}`
|
||||
: 'Failed to prepare execution command';
|
||||
} else {
|
||||
// For dry-run, just show that we would execute
|
||||
started = true;
|
||||
executionOutput = 'Dry run - task would be executed';
|
||||
// Also prepare command for dry run display
|
||||
command = await this.prepareExecutionCommand(task, subtask);
|
||||
}
|
||||
|
||||
return {
|
||||
task,
|
||||
found: true,
|
||||
started,
|
||||
subtaskId,
|
||||
subtask,
|
||||
executionOutput,
|
||||
command: command || undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
task: null,
|
||||
found: false,
|
||||
started: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for existing in-progress tasks and determine conflicts
|
||||
*/
|
||||
async checkInProgressConflicts(
|
||||
targetTaskId: string
|
||||
): Promise<ConflictCheckResult> {
|
||||
const allTasks = await this.taskService.getTaskList();
|
||||
const inProgressTasks = allTasks.tasks.filter(
|
||||
(task) => task.status === 'in-progress'
|
||||
);
|
||||
|
||||
// If the target task is already in-progress, that's fine
|
||||
const targetTaskInProgress = inProgressTasks.find(
|
||||
(task) => task.id === targetTaskId
|
||||
);
|
||||
if (targetTaskInProgress) {
|
||||
return { canProceed: true, conflictingTasks: [] };
|
||||
}
|
||||
|
||||
// Check if target is a subtask and its parent is in-progress
|
||||
const isSubtask = targetTaskId.includes('.');
|
||||
if (isSubtask) {
|
||||
const parentTaskId = targetTaskId.split('.')[0];
|
||||
const parentInProgress = inProgressTasks.find(
|
||||
(task) => task.id === parentTaskId
|
||||
);
|
||||
if (parentInProgress) {
|
||||
return { canProceed: true, conflictingTasks: [] }; // Allow subtasks when parent is in-progress
|
||||
}
|
||||
}
|
||||
|
||||
// Check if other unrelated tasks are in-progress
|
||||
const conflictingTasks = inProgressTasks.filter((task) => {
|
||||
if (task.id === targetTaskId) return false;
|
||||
|
||||
// If target is a subtask, exclude its parent from conflicts
|
||||
if (isSubtask) {
|
||||
const parentTaskId = targetTaskId.split('.')[0];
|
||||
if (task.id === parentTaskId) return false;
|
||||
}
|
||||
|
||||
// If the in-progress task is a subtask of our target parent, exclude it
|
||||
if (task.id.toString().includes('.')) {
|
||||
const taskParentId = task.id.toString().split('.')[0];
|
||||
if (isSubtask && taskParentId === targetTaskId.split('.')[0]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (conflictingTasks.length > 0) {
|
||||
return {
|
||||
canProceed: false,
|
||||
conflictingTasks,
|
||||
reason: 'Other tasks are already in progress'
|
||||
};
|
||||
}
|
||||
|
||||
return { canProceed: true, conflictingTasks: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available task to start
|
||||
*/
|
||||
async getNextAvailableTask(): Promise<string | null> {
|
||||
const nextTask = await this.taskService.getNextTask();
|
||||
return nextTask?.id || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a task ID to determine if it's a subtask and extract components
|
||||
*/
|
||||
private parseTaskId(taskId: string): {
|
||||
parentId: string;
|
||||
subtaskId?: string;
|
||||
} {
|
||||
if (taskId.includes('.')) {
|
||||
const [parentId, subtaskId] = taskId.split('.');
|
||||
return { parentId, subtaskId };
|
||||
}
|
||||
return { parentId: taskId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task can be started (no conflicts)
|
||||
*/
|
||||
async canStartTask(taskId: string, force = false): Promise<boolean> {
|
||||
if (force) return true;
|
||||
|
||||
const conflictCheck = await this.checkInProgressConflicts(taskId);
|
||||
return conflictCheck.canProceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare execution command for the CLI to run
|
||||
*/
|
||||
private async prepareExecutionCommand(
|
||||
task: Task,
|
||||
subtask?: any
|
||||
): Promise<{ executable: string; args: string[]; cwd: string } | null> {
|
||||
try {
|
||||
// Format the task into a prompt
|
||||
const taskPrompt = this.formatTaskPrompt(task, subtask);
|
||||
|
||||
// Use claude command - could be extended for other executors
|
||||
const executable = 'claude';
|
||||
const args = [taskPrompt];
|
||||
const cwd = process.cwd(); // or could get from project root
|
||||
|
||||
return { executable, args, cwd };
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to prepare execution command: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format task into a prompt suitable for execution
|
||||
*/
|
||||
private formatTaskPrompt(task: Task, subtask?: any): string {
|
||||
const workItem = subtask || task;
|
||||
const itemType = subtask ? 'Subtask' : 'Task';
|
||||
const itemId = subtask ? `${task.id}.${subtask.id}` : task.id;
|
||||
|
||||
let prompt = `${itemType} #${itemId}: ${workItem.title}\n\n`;
|
||||
|
||||
if (workItem.description) {
|
||||
prompt += `Description:\n${workItem.description}\n\n`;
|
||||
}
|
||||
|
||||
if (workItem.details) {
|
||||
prompt += `Implementation Details:\n${workItem.details}\n\n`;
|
||||
}
|
||||
|
||||
if (workItem.testStrategy) {
|
||||
prompt += `Test Strategy:\n${workItem.testStrategy}\n\n`;
|
||||
}
|
||||
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
prompt += `Dependencies: ${task.dependencies.join(', ')}\n\n`;
|
||||
}
|
||||
|
||||
prompt += `Please help me implement this ${itemType.toLowerCase()}.`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task with subtask resolution
|
||||
*/
|
||||
async getTaskWithSubtask(
|
||||
taskId: string
|
||||
): Promise<{ task: Task | null; subtask?: any; subtaskId?: string }> {
|
||||
const { parentId, subtaskId } = this.parseTaskId(taskId);
|
||||
|
||||
const task = await this.taskService.getTask(parentId);
|
||||
if (!task) {
|
||||
return { task: null };
|
||||
}
|
||||
|
||||
if (subtaskId && task.subtasks) {
|
||||
const subtask = task.subtasks.find((st) => String(st.id) === subtaskId);
|
||||
return { task, subtask, subtaskId };
|
||||
}
|
||||
|
||||
return { task };
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export class TaskService {
|
||||
tasks,
|
||||
total: rawTasks.length,
|
||||
filtered: filteredEntities.length,
|
||||
tag: tag, // Return the actual tag being used (either explicitly provided or active tag)
|
||||
tag: options.tag, // Only include tag if explicitly provided
|
||||
storageType: this.getStorageType()
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,127 +219,43 @@ export class TaskService {
|
||||
|
||||
/**
|
||||
* Get next available task to work on
|
||||
* Prioritizes eligible subtasks from in-progress parent tasks before falling back to top-level tasks
|
||||
*/
|
||||
async getNextTask(tag?: string): Promise<Task | null> {
|
||||
const result = await this.getTaskList({
|
||||
tag,
|
||||
filter: {
|
||||
status: ['pending', 'in-progress', 'done']
|
||||
status: ['pending', 'in-progress']
|
||||
}
|
||||
});
|
||||
|
||||
const allTasks = result.tasks;
|
||||
const priorityValues = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||
// Find tasks with no dependencies or all dependencies satisfied
|
||||
const completedIds = new Set(
|
||||
result.tasks.filter((t) => t.status === 'done').map((t) => t.id)
|
||||
);
|
||||
|
||||
// Helper to convert subtask dependencies to full dotted notation
|
||||
const toFullSubId = (
|
||||
parentId: string,
|
||||
maybeDotId: string | number
|
||||
): string => {
|
||||
if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) {
|
||||
return maybeDotId;
|
||||
const availableTasks = result.tasks.filter((task) => {
|
||||
if (task.status === 'done' || task.status === 'blocked') {
|
||||
return false;
|
||||
}
|
||||
return `${parentId}.${maybeDotId}`;
|
||||
};
|
||||
|
||||
// Build completed IDs set (both tasks and subtasks)
|
||||
const completedIds = new Set<string>();
|
||||
allTasks.forEach((t) => {
|
||||
if (t.status === 'done') {
|
||||
completedIds.add(String(t.id));
|
||||
}
|
||||
if (Array.isArray(t.subtasks)) {
|
||||
t.subtasks.forEach((st) => {
|
||||
if (st.status === 'done') {
|
||||
completedIds.add(`${t.id}.${st.id}`);
|
||||
}
|
||||
});
|
||||
if (!task.dependencies || task.dependencies.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return task.dependencies.every((depId) =>
|
||||
completedIds.has(depId.toString())
|
||||
);
|
||||
});
|
||||
|
||||
// 1) Look for eligible subtasks from in-progress parent tasks
|
||||
const candidateSubtasks: Array<Task & { parentId?: string }> = [];
|
||||
|
||||
allTasks
|
||||
.filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks))
|
||||
.forEach((parent) => {
|
||||
parent.subtasks!.forEach((st) => {
|
||||
const stStatus = (st.status || 'pending').toLowerCase();
|
||||
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
|
||||
|
||||
const fullDeps =
|
||||
st.dependencies?.map((d) => toFullSubId(String(parent.id), d)) ??
|
||||
[];
|
||||
const depsSatisfied =
|
||||
fullDeps.length === 0 ||
|
||||
fullDeps.every((depId) => completedIds.has(String(depId)));
|
||||
|
||||
if (depsSatisfied) {
|
||||
candidateSubtasks.push({
|
||||
id: `${parent.id}.${st.id}`,
|
||||
title: st.title || `Subtask ${st.id}`,
|
||||
status: st.status || 'pending',
|
||||
priority: st.priority || parent.priority || 'medium',
|
||||
dependencies: fullDeps,
|
||||
parentId: String(parent.id),
|
||||
description: st.description,
|
||||
details: st.details,
|
||||
testStrategy: st.testStrategy,
|
||||
subtasks: []
|
||||
} as Task & { parentId: string });
|
||||
}
|
||||
});
|
||||
// Sort by priority
|
||||
availableTasks.sort((a, b) => {
|
||||
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
const aPriority = priorityOrder[a.priority || 'medium'];
|
||||
const bPriority = priorityOrder[b.priority || 'medium'];
|
||||
return aPriority - bPriority;
|
||||
});
|
||||
|
||||
if (candidateSubtasks.length > 0) {
|
||||
// Sort by priority → dependency count → parent ID → subtask ID
|
||||
candidateSubtasks.sort((a, b) => {
|
||||
const pa =
|
||||
priorityValues[a.priority as keyof typeof priorityValues] ?? 2;
|
||||
const pb =
|
||||
priorityValues[b.priority as keyof typeof priorityValues] ?? 2;
|
||||
if (pb !== pa) return pb - pa;
|
||||
|
||||
if (a.dependencies!.length !== b.dependencies!.length) {
|
||||
return a.dependencies!.length - b.dependencies!.length;
|
||||
}
|
||||
|
||||
// Compare parent then subtask ID numerically
|
||||
const [aPar, aSub] = String(a.id).split('.').map(Number);
|
||||
const [bPar, bSub] = String(b.id).split('.').map(Number);
|
||||
if (aPar !== bPar) return aPar - bPar;
|
||||
return aSub - bSub;
|
||||
});
|
||||
|
||||
return candidateSubtasks[0];
|
||||
}
|
||||
|
||||
// 2) Fall back to top-level tasks (original logic)
|
||||
const eligibleTasks = allTasks.filter((task) => {
|
||||
const status = (task.status || 'pending').toLowerCase();
|
||||
if (status !== 'pending' && status !== 'in-progress') return false;
|
||||
|
||||
const deps = task.dependencies ?? [];
|
||||
return deps.every((depId) => completedIds.has(String(depId)));
|
||||
});
|
||||
|
||||
if (eligibleTasks.length === 0) return null;
|
||||
|
||||
// Sort by priority → dependency count → task ID
|
||||
const nextTask = eligibleTasks.sort((a, b) => {
|
||||
const pa = priorityValues[a.priority as keyof typeof priorityValues] ?? 2;
|
||||
const pb = priorityValues[b.priority as keyof typeof priorityValues] ?? 2;
|
||||
if (pb !== pa) return pb - pa;
|
||||
|
||||
const da = (a.dependencies ?? []).length;
|
||||
const db = (b.dependencies ?? []).length;
|
||||
if (da !== db) return da - db;
|
||||
|
||||
return Number(a.id) - Number(b.id);
|
||||
})[0];
|
||||
|
||||
return nextTask;
|
||||
return availableTasks[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,52 +360,4 @@ export class TaskService {
|
||||
async setActiveTag(tag: string): Promise<void> {
|
||||
await this.configManager.setActiveTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status - delegates to storage layer which handles storage-specific logic
|
||||
*/
|
||||
async updateTaskStatus(
|
||||
taskId: string | number,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
taskId: string;
|
||||
}> {
|
||||
// Ensure we have storage
|
||||
if (!this.storage) {
|
||||
throw new TaskMasterError(
|
||||
'Storage not initialized',
|
||||
ERROR_CODES.STORAGE_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// Use provided tag or get active tag
|
||||
const activeTag = tag || this.getActiveTag();
|
||||
const taskIdStr = String(taskId);
|
||||
|
||||
try {
|
||||
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
||||
return await this.storage.updateTaskStatus(
|
||||
taskIdStr,
|
||||
newStatus,
|
||||
activeTag
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
`Failed to update task status for ${taskIdStr}`,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
{
|
||||
operation: 'updateTaskStatus',
|
||||
resource: 'task',
|
||||
taskId: taskIdStr,
|
||||
newStatus,
|
||||
tag: activeTag
|
||||
},
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,9 @@
|
||||
|
||||
import type {
|
||||
IStorage,
|
||||
StorageStats,
|
||||
UpdateStatusResult
|
||||
StorageStats
|
||||
} from '../interfaces/storage.interface.js';
|
||||
import type {
|
||||
Task,
|
||||
TaskMetadata,
|
||||
TaskTag,
|
||||
TaskStatus
|
||||
} from '../types/index.js';
|
||||
import type { Task, TaskMetadata, TaskTag } from '../types/index.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
||||
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
||||
@@ -229,6 +223,14 @@ export class ApiStorage implements IStorage {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
if (tag) {
|
||||
// Check if task is in tag
|
||||
const tagData = this.tagsCache.get(tag);
|
||||
if (!tagData || !tagData.tasks.includes(taskId)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.retryOperation(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
);
|
||||
@@ -491,62 +493,6 @@ export class ApiStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID - for API storage
|
||||
*/
|
||||
async updateTaskStatus(
|
||||
taskId: string,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<UpdateStatusResult> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const existingTask = await this.retryOperation(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
);
|
||||
|
||||
if (!existingTask) {
|
||||
throw new Error(`Task ${taskId} not found`);
|
||||
}
|
||||
|
||||
const oldStatus = existingTask.status;
|
||||
if (oldStatus === newStatus) {
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId
|
||||
};
|
||||
}
|
||||
|
||||
// Update the task/subtask status
|
||||
await this.retryOperation(() =>
|
||||
this.repository.updateTask(this.projectId, taskId, {
|
||||
status: newStatus,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
|
||||
// Note: Parent status auto-adjustment is handled by the backend API service
|
||||
// which has its own business logic for managing task relationships
|
||||
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to update task status via API',
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available tags
|
||||
*/
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
* @fileoverview Refactored file-based storage implementation for Task Master
|
||||
*/
|
||||
|
||||
import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
|
||||
import type { Task, TaskMetadata } from '../../types/index.js';
|
||||
import type {
|
||||
IStorage,
|
||||
StorageStats,
|
||||
UpdateStatusResult
|
||||
StorageStats
|
||||
} from '../../interfaces/storage.interface.js';
|
||||
import { FormatHandler } from './format-handler.js';
|
||||
import { FileOperations } from './file-operations.js';
|
||||
@@ -103,70 +102,6 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tasks for a specific tag in the single tasks.json file
|
||||
*/
|
||||
@@ -324,7 +259,7 @@ export class FileStorage implements IStorage {
|
||||
tag?: string
|
||||
): Promise<void> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
|
||||
const taskIndex = tasks.findIndex((t) => t.id === taskId.toString());
|
||||
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task ${taskId} not found`);
|
||||
@@ -333,167 +268,17 @@ export class FileStorage implements IStorage {
|
||||
tasks[taskIndex] = {
|
||||
...tasks[taskIndex],
|
||||
...updates,
|
||||
id: String(taskId) // Keep consistent with normalizeTaskIds
|
||||
id: taskId.toString()
|
||||
};
|
||||
await this.saveTasks(tasks, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
|
||||
*/
|
||||
async updateTaskStatus(
|
||||
taskId: string,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<UpdateStatusResult> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
|
||||
// Check if this is a subtask (contains a dot)
|
||||
if (taskId.includes('.')) {
|
||||
return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag);
|
||||
}
|
||||
|
||||
// Handle regular task update
|
||||
const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
|
||||
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task ${taskId} not found`);
|
||||
}
|
||||
|
||||
const oldStatus = tasks[taskIndex].status;
|
||||
if (oldStatus === newStatus) {
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId: String(taskId)
|
||||
};
|
||||
}
|
||||
|
||||
tasks[taskIndex] = {
|
||||
...tasks[taskIndex],
|
||||
status: newStatus,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await this.saveTasks(tasks, tag);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId: String(taskId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update subtask status within file storage - handles parent status auto-adjustment
|
||||
*/
|
||||
private async updateSubtaskStatusInFile(
|
||||
tasks: Task[],
|
||||
subtaskId: string,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<UpdateStatusResult> {
|
||||
// Parse the subtask ID to get parent ID and subtask ID
|
||||
const parts = subtaskId.split('.');
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId`
|
||||
);
|
||||
}
|
||||
|
||||
const [parentId, subIdRaw] = parts;
|
||||
const subId = subIdRaw.trim();
|
||||
if (!/^\d+$/.test(subId)) {
|
||||
throw new Error(
|
||||
`Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.`
|
||||
);
|
||||
}
|
||||
const subtaskNumericId = Number(subId);
|
||||
|
||||
// Find the parent task
|
||||
const parentTaskIndex = tasks.findIndex(
|
||||
(t) => String(t.id) === String(parentId)
|
||||
);
|
||||
|
||||
if (parentTaskIndex === -1) {
|
||||
throw new Error(`Parent task ${parentId} not found`);
|
||||
}
|
||||
|
||||
const parentTask = tasks[parentTaskIndex];
|
||||
|
||||
// Find the subtask within the parent task
|
||||
const subtaskIndex = parentTask.subtasks.findIndex(
|
||||
(st) => st.id === subtaskNumericId || String(st.id) === subId
|
||||
);
|
||||
|
||||
if (subtaskIndex === -1) {
|
||||
throw new Error(
|
||||
`Subtask ${subtaskId} not found in parent task ${parentId}`
|
||||
);
|
||||
}
|
||||
|
||||
const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending';
|
||||
if (oldStatus === newStatus) {
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId: subtaskId
|
||||
};
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Update the subtask status
|
||||
parentTask.subtasks[subtaskIndex] = {
|
||||
...parentTask.subtasks[subtaskIndex],
|
||||
status: newStatus,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
// Auto-adjust parent status based on subtask statuses
|
||||
const subs = parentTask.subtasks;
|
||||
let parentNewStatus = parentTask.status;
|
||||
if (subs.length > 0) {
|
||||
const norm = (s: any) => s.status || 'pending';
|
||||
const isDoneLike = (s: any) => {
|
||||
const st = norm(s);
|
||||
return st === 'done' || st === 'completed';
|
||||
};
|
||||
const allDone = subs.every(isDoneLike);
|
||||
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
|
||||
const anyDone = subs.some(isDoneLike);
|
||||
if (allDone) parentNewStatus = 'done';
|
||||
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
|
||||
}
|
||||
|
||||
// Always bump updatedAt; update status only if changed
|
||||
tasks[parentTaskIndex] = {
|
||||
...parentTask,
|
||||
...(parentNewStatus !== parentTask.status
|
||||
? { status: parentNewStatus }
|
||||
: {}),
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
await this.saveTasks(tasks, tag);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
taskId: subtaskId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a task
|
||||
*/
|
||||
async deleteTask(taskId: string, tag?: string): Promise<void> {
|
||||
const tasks = await this.loadTasks(tag);
|
||||
const filteredTasks = tasks.filter((t) => String(t.id) !== String(taskId));
|
||||
const filteredTasks = tasks.filter((t) => t.id !== taskId);
|
||||
|
||||
if (filteredTasks.length === tasks.length) {
|
||||
throw new Error(`Task ${taskId} not found`);
|
||||
|
||||
@@ -8,12 +8,6 @@ import {
|
||||
type TaskListResult as ListTasksResult,
|
||||
type GetTaskListOptions
|
||||
} from './services/task-service.js';
|
||||
import {
|
||||
TaskExecutionService,
|
||||
type StartTaskOptions,
|
||||
type StartTaskResult,
|
||||
type ConflictCheckResult
|
||||
} from './services/task-execution-service.js';
|
||||
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
||||
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
||||
import type {
|
||||
@@ -22,12 +16,6 @@ import type {
|
||||
TaskFilter,
|
||||
StorageType
|
||||
} from './types/index.js';
|
||||
import {
|
||||
ExecutorService,
|
||||
type ExecutorServiceOptions,
|
||||
type ExecutionResult,
|
||||
type ExecutorType
|
||||
} from './executors/index.js';
|
||||
|
||||
/**
|
||||
* Options for creating TaskMasterCore instance
|
||||
@@ -38,15 +26,10 @@ export interface TaskMasterCoreOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-export result types from services
|
||||
* Re-export result types from TaskService
|
||||
*/
|
||||
export type { TaskListResult as ListTasksResult } from './services/task-service.js';
|
||||
export type { GetTaskListOptions } from './services/task-service.js';
|
||||
export type {
|
||||
StartTaskOptions,
|
||||
StartTaskResult,
|
||||
ConflictCheckResult
|
||||
} from './services/task-execution-service.js';
|
||||
|
||||
/**
|
||||
* TaskMasterCore facade class
|
||||
@@ -55,8 +38,6 @@ export type {
|
||||
export class TaskMasterCore {
|
||||
private configManager: ConfigManager;
|
||||
private taskService: TaskService;
|
||||
private taskExecutionService: TaskExecutionService;
|
||||
private executorService: ExecutorService | null = null;
|
||||
|
||||
/**
|
||||
* Create and initialize a new TaskMasterCore instance
|
||||
@@ -79,7 +60,6 @@ export class TaskMasterCore {
|
||||
// Services will be initialized in the initialize() method
|
||||
this.configManager = null as any;
|
||||
this.taskService = null as any;
|
||||
this.taskExecutionService = null as any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,9 +86,6 @@ export class TaskMasterCore {
|
||||
// Create task service
|
||||
this.taskService = new TaskService(this.configManager);
|
||||
await this.taskService.initialize();
|
||||
|
||||
// Create task execution service
|
||||
this.taskExecutionService = new TaskExecutionService(this.taskService);
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to initialize TaskMasterCore',
|
||||
@@ -198,109 +175,10 @@ export class TaskMasterCore {
|
||||
await this.configManager.setActiveTag(tag);
|
||||
}
|
||||
|
||||
// ==================== Task Execution Methods ====================
|
||||
|
||||
/**
|
||||
* Start working on a task with comprehensive business logic
|
||||
*/
|
||||
async startTask(
|
||||
taskId: string,
|
||||
options: StartTaskOptions = {}
|
||||
): Promise<StartTaskResult> {
|
||||
return this.taskExecutionService.startTask(taskId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task can be started (no conflicts)
|
||||
*/
|
||||
async canStartTask(taskId: string, force = false): Promise<boolean> {
|
||||
return this.taskExecutionService.canStartTask(taskId, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for existing in-progress tasks and determine conflicts
|
||||
*/
|
||||
async checkInProgressConflicts(
|
||||
targetTaskId: string
|
||||
): Promise<ConflictCheckResult> {
|
||||
return this.taskExecutionService.checkInProgressConflicts(targetTaskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task with subtask resolution
|
||||
*/
|
||||
async getTaskWithSubtask(
|
||||
taskId: string
|
||||
): Promise<{ task: Task | null; subtask?: any; subtaskId?: string }> {
|
||||
return this.taskExecutionService.getTaskWithSubtask(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available task to start
|
||||
*/
|
||||
async getNextAvailableTask(): Promise<string | null> {
|
||||
return this.taskExecutionService.getNextAvailableTask();
|
||||
}
|
||||
|
||||
// ==================== Executor Service Methods ====================
|
||||
|
||||
/**
|
||||
* Initialize executor service (lazy initialization)
|
||||
*/
|
||||
private getExecutorService(): ExecutorService {
|
||||
if (!this.executorService) {
|
||||
const executorOptions: ExecutorServiceOptions = {
|
||||
projectRoot: this.configManager.getProjectRoot()
|
||||
};
|
||||
this.executorService = new ExecutorService(executorOptions);
|
||||
}
|
||||
return this.executorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task
|
||||
*/
|
||||
async executeTask(
|
||||
task: Task,
|
||||
executorType?: ExecutorType
|
||||
): Promise<ExecutionResult> {
|
||||
const executor = this.getExecutorService();
|
||||
return executor.executeTask(task, executorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current task execution
|
||||
*/
|
||||
async stopCurrentTask(): Promise<void> {
|
||||
if (this.executorService) {
|
||||
await this.executorService.stopCurrentTask();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status
|
||||
*/
|
||||
async updateTaskStatus(
|
||||
taskId: string | number,
|
||||
newStatus: TaskStatus,
|
||||
tag?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
oldStatus: TaskStatus;
|
||||
newStatus: TaskStatus;
|
||||
taskId: string;
|
||||
}> {
|
||||
return this.taskService.updateTaskStatus(taskId, newStatus, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and cleanup resources
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// Stop any running executors
|
||||
if (this.executorService) {
|
||||
await this.executorService.stopCurrentTask();
|
||||
}
|
||||
// TaskService handles storage cleanup internally
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ export type TaskStatus =
|
||||
| 'deferred'
|
||||
| 'cancelled'
|
||||
| 'blocked'
|
||||
| 'review'
|
||||
| 'completed';
|
||||
| 'review';
|
||||
|
||||
/**
|
||||
* Task priority levels
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Tests for executor functionality
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
ExecutorFactory,
|
||||
ClaudeExecutor,
|
||||
type ExecutorOptions
|
||||
} from '../../src/executors/index.js';
|
||||
|
||||
describe('ExecutorFactory', () => {
|
||||
const mockProjectRoot = '/test/project';
|
||||
|
||||
it('should create a Claude executor', () => {
|
||||
const options: ExecutorOptions = {
|
||||
type: 'claude',
|
||||
projectRoot: mockProjectRoot
|
||||
};
|
||||
|
||||
const executor = ExecutorFactory.create(options);
|
||||
expect(executor).toBeInstanceOf(ClaudeExecutor);
|
||||
});
|
||||
|
||||
it('should throw error for unimplemented executor types', () => {
|
||||
const options: ExecutorOptions = {
|
||||
type: 'shell',
|
||||
projectRoot: mockProjectRoot
|
||||
};
|
||||
|
||||
expect(() => ExecutorFactory.create(options)).toThrow(
|
||||
'Shell executor not yet implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should get available executor types', () => {
|
||||
const types = ExecutorFactory.getAvailableTypes();
|
||||
expect(types).toContain('claude');
|
||||
expect(types).toContain('shell');
|
||||
expect(types).toContain('custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClaudeExecutor', () => {
|
||||
const mockProjectRoot = '/test/project';
|
||||
let executor: ClaudeExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
executor = new ClaudeExecutor(mockProjectRoot);
|
||||
});
|
||||
|
||||
it('should return claude as executor type', () => {
|
||||
expect(executor.getType()).toBe('claude');
|
||||
});
|
||||
|
||||
it('should format task prompt correctly', () => {
|
||||
const mockTask = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test description',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
dependencies: [],
|
||||
details: 'Implementation details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
// Access protected method through any type assertion for testing
|
||||
const formattedPrompt = (executor as any).formatTaskPrompt(mockTask);
|
||||
|
||||
expect(formattedPrompt).toContain('Task ID: 1');
|
||||
expect(formattedPrompt).toContain('Title: Test Task');
|
||||
expect(formattedPrompt).toContain('Description:\nTest description');
|
||||
expect(formattedPrompt).toContain(
|
||||
'Implementation Details:\nImplementation details'
|
||||
);
|
||||
expect(formattedPrompt).toContain('Test Strategy:\nUnit tests');
|
||||
expect(formattedPrompt).toContain('Status: pending');
|
||||
expect(formattedPrompt).toContain('Priority: high');
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,7 @@ Task Master provides an MCP server that Claude Code can connect to. Configure in
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_key_here",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user