Compare commits

..

4 Commits

Author SHA1 Message Date
Ralph Khreish
d4826e0258 wip 2025-09-08 12:33:43 -07:00
Ralph Khreish
b9e3eecafe chore: address oauth PR concerns (#1184) 2025-09-08 01:15:19 +02:00
Ralph Khreish
6dd910fc52 feat: add oauth with remote server (#1178) 2025-09-04 20:45:41 +02:00
Ralph Khreish
19ec52181d feat: create tm-core and apps/cli (#1093)
- add typescript
- add npm workspaces
2025-09-01 21:44:43 +02:00
283 changed files with 11379 additions and 22762 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Improve `analyze-complexity` cli docs and `--research` flag documentation

View 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

View File

@@ -8,8 +8,10 @@
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"docs"
]

View 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.

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": minor
---
Add Cursor IDE custom slash command support
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.

View 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

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Change parent task back to "pending" when all subtasks are in "pending" state

View File

@@ -1,13 +0,0 @@
---
"task-master-ai": minor
---
Enhanced Roo Code profile with MCP timeout configuration for improved reliability during long-running AI operations. The Roo profile now automatically configures a 300-second timeout for MCP server operations, preventing timeouts during complex tasks like `parse-prd`, `expand-all`, `analyze-complexity`, and `research` operations. This change also replaces static MCP configuration files with programmatic generation for better maintainability.
**What's New:**
- 300-second timeout for MCP operations (up from default 60 seconds)
- Programmatic MCP configuration generation (replaces static asset files)
- Enhanced reliability for AI-powered operations
- Consistent with other AI coding assistant profiles
**Migration:** No user action required - existing Roo Code installations will automatically receive the enhanced MCP configuration on next initialization.

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix Claude Code settings validation for pathToClaudeCodeExecutable

16
.changeset/pre.json Normal file
View File

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

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix sonar deep research model failing, should be called `sonar-deep-research`

View 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.

View 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
---

View 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

View File

@@ -1,245 +0,0 @@
---
name: surgical-test-generator
description: Use this agent when you need to generate comprehensive, production-grade test suites for existing code. This includes: analyzing code paths and failure modes, detecting untestable patterns, generating unit/integration tests, following project test conventions, focusing on real-world edge cases, and ensuring deterministic test execution. The agent specializes in catching bugs that matter, not inflating coverage metrics.
Examples:
<example>
Context: The user wants tests for a specific function they just wrote.
user: "I need tests for this validatePayment function that handles credit card processing"
assistant: "I'll use the surgical-test-generator agent to analyze the validatePayment function's code paths, identify critical edge cases like invalid card numbers and network failures, and generate comprehensive tests following your project's conventions."
<commentary>
User needs specific function testing - the agent will analyze paths, boundaries, and failure modes for payment validation.
</commentary>
</example>
<example>
Context: The user has untested legacy code that needs coverage.
user: "We have this 500-line OrderProcessor class with zero tests and it's causing production issues"
assistant: "I'll invoke the surgical-test-generator agent to map all the code paths in OrderProcessor, prioritize tests for the riskiest behaviors, and generate a test suite that catches the bugs most likely to cause production failures."
<commentary>
Legacy code without tests - the agent will prioritize high-risk paths and realistic failure scenarios over complete coverage.
</commentary>
</example>
<example>
Context: The user wants to improve test quality, not just coverage.
user: "Our tests pass but we still get production bugs. Can you review this API handler and suggest better tests?"
assistant: "I'll use the surgical-test-generator agent to identify gaps in your current test coverage, focusing on real-world edge cases like concurrent requests, malformed input, and external service failures that often slip through basic tests."
<commentary>
Existing tests missing edge cases - the agent will focus on adversarial scenarios and production-like failures.
</commentary>
</example>
model: opus
color: blue
---
You are a principal software engineer specializing in surgical, high-signal test generation. You write tests that catch real bugs before they reach production, focusing on actual failure modes over coverage metrics. You reason about control flow, data flow, mutation, concurrency, and security to design tests that surface defects early.
## Core Capabilities
### Multi-Perspective Analysis
You sequentially analyze code through five expert lenses:
1. **Context Profiling**: Identify language, frameworks, build tools, and existing test patterns
2. **Path Analysis**: Map all reachable paths including happy, error, and exceptional flows
3. **Adversarial Thinking**: Enumerate realistic failures, boundaries, and misuse patterns
4. **Risk Prioritization**: Rank by production impact and likelihood, discard speculative cases
5. **Test Scaffolding**: Generate deterministic, isolated tests following project conventions
### Edge Case Expertise
You focus on failures that actually occur in production:
- **Data Issues**: Null/undefined, empty collections, malformed UTF-8, mixed line endings
- **Numeric Boundaries**: -1, 0, 1, MAX values, floating-point precision, integer overflow
- **Temporal Pitfalls**: DST transitions, leap years, timezone bugs, date parsing ambiguities
- **Collection Problems**: Off-by-one errors, concurrent modification, performance at scale
- **State Violations**: Out-of-order calls, missing initialization, partial updates
- **External Failures**: Network timeouts, malformed responses, retry storms, connection exhaustion
- **Concurrency Bugs**: Race conditions, deadlocks, promise leaks, thread starvation
- **Resource Limits**: Memory spikes, file descriptor leaks, connection pool saturation
- **Security Surfaces**: Injection attacks, path traversal, privilege escalation
### Framework Intelligence
You auto-detect and follow existing test patterns:
- **JavaScript/TypeScript**: Jest, Vitest, Mocha, or project wrappers
- **Python**: pytest, unittest with appropriate fixtures
- **Java/Kotlin**: JUnit 5, TestNG with proper annotations
- **C#/.NET**: xUnit.net preferred, NUnit/MSTest if dominant
- **Go**: Built-in testing package with table-driven tests
- **Rust**: Standard #[test] with proptest for properties
- **Swift**: XCTest or Swift Testing based on project
- **C/C++**: GoogleTest or Catch2 matching build system
## Your Workflow
### Phase 1: Code Analysis
You examine the provided code to understand:
- Public API surfaces and contracts
- Internal helper functions and their criticality
- External dependencies and I/O operations
- State management and mutation patterns
- Error handling and recovery paths
- Concurrency and async operations
### Phase 2: Test Strategy Development
You determine the optimal testing approach:
- Start from public APIs, work inward to critical helpers
- Test behavior not implementation unless white-box needed
- Prefer property-based tests for algebraic domains
- Use minimal stubs/mocks, prefer in-memory fakes
- Flag untestable code with refactoring suggestions
- Include stress tests for concurrency issues
### Phase 3: Test Generation
You create tests that:
- Follow project's exact style and conventions
- Use clear Arrange-Act-Assert or Given-When-Then
- Execute in under 100ms without external calls
- Remain deterministic with seeded randomness
- Self-document through descriptive names
- Explain failures clearly with context
## Detection Patterns
### When analyzing a pure function:
- Test boundary values and type edges
- Verify mathematical properties hold
- Check error propagation
- Consider numeric overflow/underflow
### When analyzing stateful code:
- Test initialization sequences
- Verify state transitions
- Check concurrent access patterns
- Validate cleanup and teardown
### When analyzing I/O operations:
- Test success paths with valid data
- Simulate network failures and timeouts
- Check malformed input handling
- Verify resource cleanup on errors
### When analyzing async code:
- Test promise resolution and rejection
- Check cancellation handling
- Verify timeout behavior
- Validate error propagation chains
## Test Quality Standards
### Coverage Philosophy
You prioritize catching real bugs over metrics:
- Critical paths get comprehensive coverage
- Edge cases get targeted scenarios
- Happy paths get basic validation
- Speculative cases get skipped
### Test Independence
Each test you generate:
- Runs in isolation without shared state
- Cleans up all resources
- Uses fixed seeds for randomness
- Mocks time when necessary
### Failure Diagnostics
Your tests provide clear failure information:
- Descriptive test names that explain intent
- Assertions that show expected vs actual
- Context about what was being tested
- Hints about likely failure causes
## Special Considerations
### When NOT to Generate Tests
You recognize when testing isn't valuable:
- Generated code that's guaranteed correct
- Simple getters/setters without logic
- Framework boilerplate
- Code scheduled for deletion
### Untestable Code Patterns
You identify and flag:
- Hard-coded external dependencies
- Global state mutations
- Time-dependent behavior without injection
- Random behavior without seeds
### Performance Testing
When relevant, you include:
- Benchmarks for critical paths
- Memory usage validation
- Concurrent load testing
- Resource leak detection
## Output Format
You generate test code that:
```[language]
// Clear test description
test('should [specific behavior] when [condition]', () => {
// Arrange - Set up test data and dependencies
// Act - Execute the code under test
// Assert - Verify the outcome
});
```
## Framework-Specific Patterns
### JavaScript/TypeScript (Jest/Vitest)
- Use describe blocks for grouping
- Leverage beforeEach/afterEach for setup
- Mock modules with jest.mock()/vi.fn, vi.mock or vi.spyOn
- Use expect matchers appropriately
### Python (pytest)
- Use fixtures for reusable setup
- Apply parametrize for data-driven tests
- Leverage pytest markers for categorization
- Use clear assertion messages
### Java (JUnit 5)
- Apply appropriate annotations
- Use nested classes for grouping
- Leverage parameterized tests
- Include display names
### C# (xUnit)
- Use Theory for data-driven tests
- Apply traits for categorization
- Leverage fixtures for setup
- Use fluent assertions when available
## Request Handling
### Specific Test Requests
When asked for specific tests:
- Focus only on the requested scope
- Don't generate broader coverage unless asked
- Provide targeted, high-value scenarios
- Include rationale for test choices
### Comprehensive Coverage Requests
When asked for full coverage:
- Start with critical paths
- Add edge cases progressively
- Group related tests logically
- Flag if multiple files needed
### Legacy Code Testing
When testing untested code:
- Prioritize high-risk areas first
- Add characterization tests
- Suggest refactoring for testability
- Focus on preventing regressions
## Communication Guidelines
You always:
- Explain why each test matters
- Document test intent clearly
- Note any assumptions made
- Highlight untestable patterns
- Suggest improvements when relevant
- Follow existing project style exactly
- Generate only essential test code
When you need additional context like test frameworks or existing patterns, you ask specifically for those files. You focus on generating tests that will actually catch bugs in production, not tests that merely increase coverage numbers. Every test you write has a clear purpose and tests a realistic scenario.

View File

@@ -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",

View File

@@ -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();

View File

@@ -6,124 +6,73 @@ on:
- main
- next
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
branches:
- main
- next
permissions:
contents: read
env:
DO_NOT_TRACK: 1
NODE_ENV: development
jobs:
# Fast checks that can run in parallel
format-check:
name: Format Check
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
cache: 'npm'
- name: Install dependencies
run: npm install --frozen-lockfile --prefer-offline
timeout-minutes: 5
- name: Install Dependencies
id: install
run: npm ci
timeout-minutes: 2
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
format-check:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Restore node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Format Check
run: npm run format-check
env:
FORCE_COLOR: 1
typecheck:
name: Typecheck
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm install --frozen-lockfile --prefer-offline
timeout-minutes: 5
- name: Typecheck
run: npm run turbo:typecheck
env:
FORCE_COLOR: 1
# Build job to ensure everything compiles
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm install --frozen-lockfile --prefer-offline
timeout-minutes: 5
- name: Build
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: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: dist/
retention-days: 1
test:
name: Test
timeout-minutes: 15
needs: setup
runs-on: ubuntu-latest
needs: [format-check, typecheck, build]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm install --frozen-lockfile --prefer-offline
timeout-minutes: 5
- name: Download build artifacts
uses: actions/download-artifact@v4
- name: Restore node_modules
uses: actions/cache@v4
with:
name: build-artifacts
path: dist/
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Run Tests
run: |
@@ -132,6 +81,7 @@ jobs:
NODE_ENV: test
CI: true
FORCE_COLOR: 1
timeout-minutes: 10
- name: Upload Test Results
if: always()

View File

@@ -1,57 +0,0 @@
name: Trigger Claude Documentation Update
on:
push:
branches:
- next
paths-ignore:
- "apps/docs/**"
- "*.md"
- ".github/workflows/**"
jobs:
trigger-docs-update:
# Only run if changes were merged (not direct pushes from bots)
if: github.actor != 'github-actions[bot]' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need previous commit for comparison
- name: Get changed files
id: changed-files
run: |
echo "Changed files in this push:"
git diff --name-only HEAD^ HEAD | tee changed_files.txt
# Store changed files for Claude to analyze (escaped for JSON)
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD | jq -Rs .)
echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT
# Get the commit message (escaped for JSON)
COMMIT_MSG=$(git log -1 --pretty=%B | jq -Rs .)
echo "commit_message=$COMMIT_MSG" >> $GITHUB_OUTPUT
# Get diff for documentation context (escaped for JSON)
COMMIT_DIFF=$(git diff HEAD^ HEAD --stat | jq -Rs .)
echo "commit_diff=$COMMIT_DIFF" >> $GITHUB_OUTPUT
# Get commit SHA
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Trigger Claude workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Trigger the Claude docs updater workflow with the change information
gh workflow run claude-docs-updater.yml \
--ref next \
-f commit_sha="${{ steps.changed-files.outputs.commit_sha }}" \
-f commit_message=${{ steps.changed-files.outputs.commit_message }} \
-f changed_files=${{ steps.changed-files.outputs.changed_files }} \
-f commit_diff=${{ steps.changed-files.outputs.commit_diff }}

View File

@@ -1,27 +1,18 @@
name: Claude Documentation Updater
on:
workflow_dispatch:
inputs:
commit_sha:
description: 'The commit SHA that triggered this update'
required: true
type: string
commit_message:
description: 'The commit message'
required: true
type: string
changed_files:
description: 'List of changed files'
required: true
type: string
commit_diff:
description: 'Diff summary of changes'
required: true
type: string
push:
branches:
- next
paths-ignore:
- "apps/docs/**"
- "*.md"
- ".github/workflows/**"
jobs:
update-docs:
# Only run if changes were merged (not direct pushes from bots)
if: github.actor != 'github-actions[bot]' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -31,8 +22,28 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: next
fetch-depth: 0 # Need full history to checkout specific commit
fetch-depth: 2 # Need previous commit for comparison
- name: Get changed files
id: changed-files
run: |
echo "Changed files in this push:"
git diff --name-only HEAD^ HEAD | tee changed_files.txt
# Store changed files for Claude to analyze
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
git diff --name-only HEAD^ HEAD >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Get the commit message and changes summary
echo "commit_message<<EOF" >> $GITHUB_OUTPUT
git log -1 --pretty=%B >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Get diff for documentation context
echo "commit_diff<<EOF" >> $GITHUB_OUTPUT
git diff HEAD^ HEAD --stat >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create docs update branch
id: create-branch
@@ -60,12 +71,12 @@ jobs:
You are a documentation specialist. Analyze the recent changes pushed to the 'next' branch and update the documentation accordingly.
Recent changes:
- Commit: ${{ inputs.commit_message }}
- Commit: ${{ steps.changed-files.outputs.commit_message }}
- Changed files:
${{ inputs.changed_files }}
${{ steps.changed-files.outputs.changed_files }}
- Changes summary:
${{ inputs.commit_diff }}
${{ steps.changed-files.outputs.commit_diff }}
Your task:
1. Analyze the changes to understand what functionality was added, modified, or removed
@@ -102,7 +113,7 @@ jobs:
This PR was automatically generated to update documentation based on recent changes.
Original commit: ${{ inputs.commit_message }}
Original commit: ${{ steps.changed-files.outputs.commit_message }}
Co-authored-by: Claude <claude-assistant@anthropic.com>"
fi
@@ -122,12 +133,12 @@ jobs:
This PR automatically updates documentation based on recent changes merged to the \`next\` branch.
### Original Changes
**Commit:** ${{ inputs.commit_sha }}
**Message:** ${{ inputs.commit_message }}
**Commit:** ${{ github.sha }}
**Message:** ${{ steps.changed-files.outputs.commit_message }}
### Changed Files in Original Commit
\`\`\`
${{ inputs.changed_files }}
${{ steps.changed-files.outputs.changed_files }}
\`\`\`
### Documentation Updates

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

5
.gitignore vendored
View File

@@ -93,7 +93,4 @@ dev-debug.log
apps/extension/.vscode-test/
# apps/extension
apps/extension/vsix-build/
# turbo
.turbo
apps/extension/vsix-build/

View File

@@ -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",

View File

@@ -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"]
}

2
.nvmrc
View File

@@ -1 +1 @@
22
22

View File

@@ -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",

View File

@@ -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": {}
}

View File

@@ -1,220 +0,0 @@
# PRD: Autonomous TDD + Git Workflow (On Rails)
## Summary
- Put the existing git and test workflows on rails: a repeatable, automated process that can run autonomously, with guardrails and a compact TUI for visibility.
- Flow: for a selected task, create a branch named with the tag + task id → generate tests for the first subtask (red) using the Surgical Test Generator → implement code (green) → verify tests → commit → repeat per subtask → final verify → push → open PR against the default branch.
- Build on existing rules: `.cursor/rules/git_workflow.mdc`, `.cursor/rules/test_workflow.mdc`, `.claude/agents/surgical-test-generator.md`, and existing CLI/core services.
## Goals
- Deterministic, resumable automation to execute the TDD loop per subtask with minimal human intervention.
- Strong guardrails: never commit to the default branch; only commit when tests pass; enforce status transitions; persist logs/state for debuggability.
- Visibility: a compact terminal UI (like lazygit) to pick tag, view tasks, and start work; right-side pane opens an executor terminal (via tmux) for agent coding.
- Extensible: framework-agnostic test generation via the Surgical Test Generator; detect and use the repos test command for execution with coverage thresholds.
## NonGoals (initial)
- Full multi-language runner parity beyond detection and executing the projects test command.
- Complex GUI; start with CLI/TUI + tmux pane. IDE/extension can hook into the same state later.
- Rich executor selection UX (codex/gemini/claude) — well prompt per run; defaults can come later.
## Success Criteria
- One command can autonomously complete a tasks subtasks via TDD and open a PR when done.
- All commits made on a branch that includes the tag and task id (see Branch Naming); no commits to the default branch directly.
- Every subtask iteration: failing tests added first (red), then code added to pass them (green), commit only after green.
- End-to-end logs + artifacts stored in `.taskmaster/reports/runs/<timestamp-or-id>/`.
## User Stories
- As a developer, I can run `tm autopilot <taskId>` and watch a structured, safe workflow execute.
- As a reviewer, I can inspect commits per subtask, and a PR summarizing the work when the task completes.
- As an operator, I can see current step, active subtask, tests status, and logs in a compact CLI view and read a final run report.
## HighLevel Workflow
1) Preflight
- Verify clean working tree or confirm staging/commit policy (configurable).
- Detect repo type and the projects test command (e.g., `npm test`, `pnpm test`, `pytest`, `go test`).
- Validate tools: `git`, `gh` (optional for PR), `node/npm`, and (if used) `claude` CLI.
- Load TaskMaster state and selected task; if no subtasks exist, automatically run “expand” before working.
2) Branch & Tag Setup
- Checkout default branch and update (optional), then create a branch using Branch Naming (below).
- Map branch ↔ tag via existing tag management; explicitly set active tag to the branchs tag.
3) Subtask Loop (for each pending/in-progress subtask in dependency order)
- Select next eligible subtask using `tm-core` TaskService `getNextTask()` and subtask eligibility logic.
- Red: generate or update failing tests for the subtask
- Use the Surgical Test Generator system prompt (`.claude/agents/surgical-test-generator.md`) to produce high-signal tests following project conventions.
- Run tests to confirm red; record results. If not red (already passing), skip to next subtask or escalate.
- Green: implement code to pass tests
- Use executor to implement changes (initial: `claude` CLI prompt with focused context).
- Re-run tests until green or timeout/backoff policy triggers.
- Commit: when green
- Commit tests + code with conventional commit message. Optionally update subtask status to `done`.
- Persist run step metadata/logs.
4) Finalization
- Run full test suite and coverage (if configured); optionally lint/format.
- Commit any final adjustments.
- Push branch (ask user to confirm); create PR (via `gh pr create`) targeting the default branch. Title format: `Task #<id> [<tag>]: <title>`.
5) PostRun
- Update task status if desired (e.g., `review`).
- Persist run report (JSON + markdown summary) to `.taskmaster/reports/runs/<run-id>/`.
## Guardrails
- Never commit to the default branch.
- Commit only if all tests (targeted and suite) pass; allow override flags.
- Enforce 80% coverage thresholds (lines/branches/functions/statements) by default; configurable.
- Timebox/model ops and retries; if not green within N attempts, pause with actionable state for resume.
- Always log actions, commands, and outcomes; include dry-run mode.
- Ask before branch creation, pushing, and opening a PR unless `--no-confirm` is set.
## Integration Points (Current Repo)
- CLI: `apps/cli` provides command structure and UI components.
- New command: `tm autopilot` (alias: `task-master autopilot`).
- Reuse UI components under `apps/cli/src/ui/components/` for headers/task details/next-task.
- Core services: `packages/tm-core`
- `TaskService` for selection, status, tags.
- `TaskExecutionService` for prompt formatting and executor prep.
- Executors: `claude` executor and `ExecutorFactory` to run external tools.
- Proposed new: `WorkflowOrchestrator` to drive the autonomous loop and emit progress events.
- Tag/Git utilities: `scripts/modules/utils/git-utils.js` and `scripts/modules/task-manager/tag-management.js` for branch→tag mapping and explicit tag switching.
- Rules: `.cursor/rules/git_workflow.mdc` and `.cursor/rules/test_workflow.mdc` to steer behavior and ensure consistency.
- Test generation prompt: `.claude/agents/surgical-test-generator.md`.
## Proposed Components
- Orchestrator (tm-core): `WorkflowOrchestrator` (new)
- State machine driving phases: Preflight → Branch/Tag → SubtaskIter (Red/Green/Commit) → Finalize → PR.
- Exposes an evented API (progress events) that the CLI can render.
- Stores run state artifacts.
- Test Runner Adapter
- Detects and runs tests via the projects test command (e.g., `npm test`), with targeted runs where feasible.
- API: runTargeted(files/pattern), runAll(), report summary (failures, duration, coverage), enforce 80% threshold by default.
- Git/PR Adapter
- Encapsulates `git` ops: branch create/checkout, add/commit, push.
- Optional `gh` integration to open PR; fallback to instructions if `gh` unavailable.
- Confirmation gates for branch creation and pushes.
- Adds commit footers and a unified trailer (`Refs: TM-<tag>-<id>[.<sub>]`) for robust mapping to tasks/subtasks.
- Prompt/Exec Adapter
- Uses existing executor service to call the selected coding assistant (initially `claude`) with tight prompts: task/subtask context, surgical tests first, then minimal code to green.
- Run State + Reporting
- JSONL log of steps, timestamps, commands, test results.
- Markdown summary for PR description and post-run artifact.
## CLI UX (MVP)
- Command: `tm autopilot [taskId]`
- Flags: `--dry-run`, `--no-push`, `--no-pr`, `--no-confirm`, `--force`, `--max-attempts <n>`, `--runner <auto|custom>`, `--commit-scope <scope>`
- Output: compact header (project, tag, branch), current phase, subtask line, last test summary, next actions.
- Resume: If interrupted, `tm autopilot --resume` picks up from last checkpoint in run state.
### TUI with tmux (Linear Execution)
- Left pane: Tag selector, task list (status/priority), start/expand shortcuts; “Start” triggers the next task or a selected task.
- Right pane: Executor terminal (tmux split) that runs the coding agent (claude-code/codex). Autopilot can hand over to the right pane during green.
- MCP integration: use MCP tools for task queries/updates and for shell/test invocations where available.
## Prompts (Initial Direction)
- Red phase prompt skeleton (tests):
- Use `.claude/agents/surgical-test-generator.md` as the system prompt to generate high-signal failing tests tailored to the projects language and conventions. Keep scope minimal and deterministic; no code changes yet.
- Green phase prompt skeleton (code):
- “Make the tests pass by changing the smallest amount of code, following project patterns. Only modify necessary files. Keep commits focused to this subtask.”
## Configuration
- `.taskmaster/config.json` additions
- `autopilot`: `{ enabled: true, requireCleanWorkingTree: true, commitTemplate: "{type}({scope}): {msg}", defaultCommitType: "feat" }`
- `test`: `{ runner: "auto", coverageThresholds: { lines: 80, branches: 80, functions: 80, statements: 80 } }`
- `git`: `{ branchPattern: "{tag}/task-{id}-{slug}", pr: { enabled: true, base: "default" }, commitFooters: { task: "Task-Id", subtask: "Subtask-Id", tag: "Tag" }, commitTrailer: "Refs: TM-{tag}-{id}{.sub?}" }`
## Decisions
- Use conventional commits plus footers and a unified trailer `Refs: TM-<tag>-<id>[.<sub>]` for all commits; Git/PR adapter is responsible for injecting these.
## Risks and Mitigations
- Model hallucination/large diffs: restrict prompt scope; enforce minimal changes; show diff previews (optional) before commit.
- Flaky tests: allow retries, isolate targeted runs for speed, then full suite before commit.
- Environment variability: detect runners/tools; provide fallbacks and actionable errors.
- PR creation fails: still push and print manual commands; persist PR body to reuse.
## Open Questions
1) Slugging rules for branch names; any length limits or normalization beyond `{slug}` token sanitize?
2) PR body standard sections beyond run report (e.g., checklist, coverage table)?
3) Default executor prompt fine-tuning once codex/gemini integration is available.
4) Where to store persistent TUI state (pane layout, last selection) in `.taskmaster/state.json`?
## Branch Naming
- Include both the tag and the task id in the branch name to make lineage explicit.
- Default pattern: `<tag>/task-<id>[-slug]` (e.g., `master/task-12`, `tag-analytics/task-4-user-auth`).
- Configurable via `.taskmaster/config.json`: `git.branchPattern` supports tokens `{tag}`, `{id}`, `{slug}`.
## PR Base Branch
- Use the repositorys default branch (detected via git) unless overridden.
- Title format: `Task #<id> [<tag>]: <title>`.
## RPG Mapping (Repository Planning Graph)
Functional nodes (capabilities):
- Autopilot Orchestration → drives TDD loop and lifecycle
- Test Generation (Surgical) → produces failing tests from subtask context
- Test Execution + Coverage → runs suite, enforces thresholds
- Git/Branch/PR Management → safe operations and PR creation
- TUI/Terminal Integration → interactive control and visibility via tmux
- MCP Integration → structured task/status/context operations
Structural nodes (code organization):
- `packages/tm-core`:
- `services/workflow-orchestrator.ts` (new)
- `services/test-runner-adapter.ts` (new)
- `services/git-adapter.ts` (new)
- existing: `task-service.ts`, `task-execution-service.ts`, `executors/*`
- `apps/cli`:
- `src/commands/autopilot.command.ts` (new)
- `src/ui/tui/` (new tmux/TUI helpers)
- `scripts/modules`:
- reuse `utils/git-utils.js`, `task-manager/tag-management.js`
- `.claude/agents/`:
- `surgical-test-generator.md`
Edges (data/control flow):
- Autopilot → Test Generation → Test Execution → Git Commit → loop
- Autopilot → Git Adapter (branch, tag, PR)
- Autopilot → TUI (event stream) → tmux pane control
- Autopilot → MCP tools for task/status updates
- Test Execution → Coverage gate → Autopilot decision
Topological traversal (implementation order):
1) Git/Test adapters (foundations)
2) Orchestrator skeleton + events
3) CLI `autopilot` command and dry-run
4) Surgical test-gen integration and execution gate
5) PR creation, run reports, resumability
## Phased Roadmap
- Phase 0: Spike
- Implement CLI skeleton `tm autopilot` with dry-run showing planned steps from a real task + subtasks.
- Detect test runner (package.json) and git state; render a preflight report.
- Phase 1: Core Rails
- Implement `WorkflowOrchestrator` in `tm-core` with event stream; add Git/Test adapters.
- Support subtask loop (red/green/commit) with framework-agnostic test generation and detected test command; commit gating on passing tests and coverage.
- Branch/tag mapping via existing tag-management APIs.
- Run report persisted under `.taskmaster/reports/runs/`.
- Phase 2: PR + Resumability
- Add `gh` PR creation with well-formed body using the run report.
- Introduce resumable checkpoints and `--resume` flag.
- Add coverage enforcement and optional lint/format step.
- Phase 3: Extensibility + Guardrails
- Add support for basic pytest/go test adapters.
- Add safeguards: diff preview mode, manual confirm gates, aggressive minimal-change prompts.
- Optional: small TUI panel and extension panel leveraging the same run state file.
## References (Repo)
- Test Workflow: `.cursor/rules/test_workflow.mdc`
- Git Workflow: `.cursor/rules/git_workflow.mdc`
- CLI: `apps/cli/src/commands/start.command.ts`, `apps/cli/src/ui/components/*.ts`
- Core Services: `packages/tm-core/src/services/task-service.ts`, `task-execution-service.ts`
- Executors: `packages/tm-core/src/executors/*`
- Git Utilities: `scripts/modules/utils/git-utils.js`
- Tag Management: `scripts/modules/task-manager/tag-management.js`
- Surgical Test Generator: `.claude/agents/surgical-test-generator.md`

View File

@@ -1,221 +0,0 @@
# PRD: RPGBased User Story Mode + ValidationFirst Delivery
## Summary
- Introduce a “User Story Mode” where each Task is a user story and each Subtask is a concrete implementation step. Enable via config flag; when enabled, Task generation and PRD parsing produce userstory titles/details with acceptance criteria, while Subtasks capture implementation details.
- Build a validationfirst delivery pipeline: derive tests from acceptance criteria (Surgical Test Generator), wire TDD rails and Git/PR mapping so reviews focus on verification rather than code spelunking.
- Keep everything on rails: branch naming with tag+task id, commit/PR linkage to tasks/subtasks, coverage + test gates, and lightweight TUI for fast execution.
## NorthStar Outcomes
- Humans stay in briefs/frontends; implementation runs quickly, often without opening the IDE.
- “Definition of Done” is expressed and enforced as tests; business logic is encoded in test criteria/acceptance criteria.
- Endtoend linkage from brief → user story → subtasks → commits/PRs → delivery, with reproducible automation and minimal ceremony.
## Problem
- The bottleneck is validation and PR review, not code generation. Plans are helpful but the chokepoint is proving correctness, business conformance, and integration.
- Current test workflow is too Jestspecific; we need frameworkagnostic generation and execution.
- We need consistent Git/TDD wiring so GitHub integrations can map work artifacts to tasks/subtasks without ambiguity.
## Solution Overview
- Add a configuration flag to switch to user story mode and adapt prompts/parsers.
- Expand tasks with explicit Acceptance Criteria and Test Criteria; drive Surgical Test Generator to create failing tests first; wire autonomous TDD loops per subtask until green, then commit.
- Enforce coverage (80% default) and generate PRs that summarize user story, acceptance criteria coverage, and test results; commits/PRs contain metadata to link back to tasks/subtasks.
- Provide a compact TUI (tmux) to pick tag/tasks and launch an executor terminal, while the orchestrator runs rails in the background.
---
## Configuration
- `.taskmaster/config.json` additions
- `stories`: `{ enabled: true, storyLabel: "User Story", acceptanceKey: "Acceptance Criteria" }`
- `autopilot`: `{ enabled: true, requireCleanWorkingTree: true }`
- `test`: `{ runner: "auto", coverageThresholds: { lines: 80, branches: 80, functions: 80, statements: 80 } }`
- `git`: `{ branchPattern: "{tag}/task-{id}-{slug}", pr: { enabled: true, base: "default" }, commitFooters: { task: "Task-Id", subtask: "Subtask-Id", tag: "Tag" } }`
Behavior when `stories.enabled=true`:
- Task generation prompts and PRD parsers produce userstory formatted titles and descriptions, include acceptance criteria blocks, and set `task.type = 'user_story'`.
- Subtasks remain implementation steps with concise technical goals.
- Expand will ensure any missing acceptance criteria is synthesized (from brief/PRD content) before starting work.
---
## Data Model Changes
- Task fields (add):
- `type: 'user_story' | 'technical'` (default `technical`)
- `acceptanceCriteria: string[] | string` (structured or markdown)
- `testCriteria: string[] | string` (optional, derived from acceptance criteria; what to validate)
- Subtask fields remain focused on implementation detail and dependency graph.
Storage and UI remain backward compatible; fields are optional when `stories.enabled=false`.
### JSON Gherkin Representation (for stories)
Add an optional `gherkin` block to Tasks in story mode. Keep Hybrid acceptanceCriteria as the human/authoring surface; maintain a normalized JSON Gherkin for deterministic mapping.
```
GherkinFeature {
id: string, // FEAT-<taskId>
name: string, // mirrors user story title
description?: string,
background?: { steps: Step[] },
scenarios: Scenario[]
}
Scenario {
id: string, // SC-<taskId>-<n> or derived from AC id
name: string,
tags?: string[],
steps: Step[], // Given/When/Then/And/But
examples?: Record<string, any>[]
}
Step { keyword: 'Given'|'When'|'Then'|'And'|'But', text: string }
```
Notes
- Derive `gherkin.scenarios` from acceptanceCriteria when obvious; preserve both raw markdown and normalized items.
- Allow crossreferences between scenarios and AC items (e.g., `refs: ['AC-12-1']`).
---
## RPG Plan (Repository Planning Graph)
Functional Nodes (Capabilities)
- Brief Intake → parse briefs/PRDs and extract user stories (when enabled)
- User Story Generation → create task title/details as user stories + acceptance criteria
- JSON Gherkin Synthesis → produce Feature/Scenario structure from acceptance criteria
- Acceptance/Test Criteria Synthesis → convert acceptance criteria into concrete test criteria
- Surgical Test Generation → generate failing tests per subtask using `.claude/agents/surgical-test-generator.md`
- Implementation Planning → expand subtasks as atomic implementation steps with dependencies
- Autonomous Execution (Rails) → branch, red/green loop per subtask, commit when green
- Validation & Review Automation → coverage gates, PR body with user story + results, checklist
- GitHub Integration Mapping → branch naming, commit footers, PR linkage to tasks/subtasks
- TUI/Terminal Integration → tag/task selection left pane; executor terminal right pane via tmux
Structural Nodes (Code Organization)
- `packages/tm-core`
- `services/workflow-orchestrator.ts` (new): drives rails, emits progress events
- `services/story-mode.service.ts` (new): toggles prompts/parsers for user stories, acceptance criteria
- `services/test-runner-adapter.ts` (new): detects/executes project test command, collects coverage
- `services/git-adapter.ts` (new): branch/commit/push, PR creation; applies commit footers
- existing: `task-service.ts`, `task-execution-service.ts`, `executors/*`
- `apps/cli`
- `src/commands/autopilot.command.ts` (new): orchestrates a full run; supports `--stories`
- `src/ui/tui/` (new): tmux helpers and compact panes for selection and logs
- `scripts/modules`
- reuse `utils/git-utils.js`, `task-manager/tag-management.js`, PR template utilities
- `.cursor/rules`
- update generation/parsing rules to emit userstory format when enabled
- `.claude/agents`
- existing: `surgical-test-generator.md` for red phase
Edges (Dependencies / Data Flow)
- Brief Intake → User Story Generation → Acceptance/Test Criteria Synthesis → Implementation Planning → Autonomous Execution → Validation/PR
- Execution ↔ Test Runner (runAll/runTargeted, coverage) → back to Execution for decisions
- Git Adapter ← Execution (commits/branch) → PR creation (target default branch)
- TUI ↔ Orchestrator (event stream) → user confirmations for branch/push/PR
- MCP Tools ↔ Orchestrator for task/status/context updates
Topological Traversal (Build Order)
1) Config + Data Model changes (stories flag, acceptance fields, optional `gherkin`)
2) Rules/Prompts updates for parsing/generation in story mode (emit AC Hybrid + JSON Gherkin)
3) Test Runner Adapter (frameworkagnostic execute + coverage)
4) Git Adapter (branch pattern `{tag}/task-{id}-{slug}`, commit footers/trailer, PR create)
5) Workflow Orchestrator wiring red/green/commit loop with coverage gate and scenario iteration
6) Surgical Test Gen integration (red) from JSON Gherkin + AC; minimalchange impl prompts (green)
7) CLI autopilot (dryrun → full run) and TUI (tmux panes)
8) PR template and review automation (user story, AC table with test links, scenarios, coverage)
---
## Git/TDD Wiring (ValidationFirst)
- Branch naming: include tag + task id (e.g., `master/task-12-user-auth`) to disambiguate context.
- Commit footers (configurable):
- `Task-Id: <id>`
- `Subtask-Id: <id>.<sub>` when relevant
- `Tag: <tag>`
- Trailer: `Refs: TM-<tag>-<id>[.<sub>] SC:<scenarioId> AC:<acId>`
- Red/Green/Commit loop per subtask:
- Red: synthesize failing tests from acceptance criteria (Surgical agent)
- Green: minimal code to pass; rerun full suite
- Commit when all tests pass and coverage ≥ 80%
- PR base: repository default branch. Title `Task #<id> [<tag>]: <title>`.
- PR body sections: User Story, Acceptance Criteria, Subtask Summary, Test Results, Coverage Table, Linked Work Items (ids), Next Steps.
---
## Prompts & Parsers (Story Mode)
- PRD/Brief Parser updates:
- Extract user stories with “As a … I want … so that …” format when present.
- Extract Acceptance Criteria as bullet list; fill gaps with LLM synthesis from brief context.
- Emit JSON Gherkin Feature/Scenarios; autosplit Given/When/Then when feasible; otherwise store text under `then` and refine later.
- Task Generation Prompt (story mode):
- “Generate a task as a User Story with clear Acceptance Criteria. Do not include implementation details in the story; produce implementation subtasks separately.”
- Subtask Generation Prompt:
- “Produce technical implementation steps to satisfy the acceptance criteria. Each subtask should be atomic and testable.”
- Test Generation (Red):
- Use `.claude/agents/surgical-test-generator.md`; seed with JSON Gherkin + Acceptance/Test Criteria; determinism favored over maximum coverage.
- Record produced test paths back into AC items and optionally scenario annotations.
- Implementation (Green):
- Minimal diffs, follow patterns, keep commits scoped to the subtask.
---
## TUI (Linear, tmuxbased)
- Left: Tag selector and task list (status/priority). Actions: Expand, Start (Next or Selected), Review.
- Right: Executor terminal (claudecode/codex) under tmux split; orchestrator logs under another pane.
- Confirmations inline (branch create, push, PR) unless `--no-confirm`.
---
## Migration & Backward Compatibility
- Optional `gherkin` block; existing tasks remain valid.
- When `stories.enabled=true`, new tasks include AC Hybrid + `gherkin`; upgrade path via a utility to synthesize both from description/testStrategy/acceptanceCriteria.
---
## Risks & Mitigations
- Hallucinated acceptance criteria → Always show criteria in PR; allow quick amend and rerun.
- Framework variance → Test Runner Adapter detects and normalizes execution/coverage; fallback to `test` script.
- Large diffs → Prompt for minimal changes; allow diff preview before commit.
- Flaky tests → Retry policy; isolate targeted runs; enforce passing full suite before commit.
---
## Acceptance Criteria Schema Options (for decision)
- Option A: Markdown only
- Pros: simple to write/edit, good for humans
- Cons: hard to map deterministically to tests; weak traceability; brittle diffs
- Option B: Structured array
- Example: `{ id, summary, given, when, then, severity, tags }`
- Pros: machinereadable; strong linking to tests/coverage; easy to diff
- Cons: heavier authoring; requires schema discipline
- Option C: Hybrid (recommended)
- Store both: a normalized array of criteria objects and a preserved `raw` markdown block
- Each criterion gets a stable `id` (e.g., `AC-<taskId>-<n>`) used in tests, commit trailers, and PR tables
- Enables clean PR tables and deterministic coverage mapping while keeping humanfriendly text
Proposed default schema (hybrid):
```
acceptanceCriteria: {
raw: """
- AC1: Guest can checkout with credit card
- AC2: Declined cards show error inline
""",
items: [
{
id: "AC-12-1",
summary: "Guest can checkout with credit card",
given: "a guest with items in cart",
when: "submits valid credit card",
then: "order is created and receipt emailed",
severity: "must",
tags: ["checkout", "payments"],
tests: [] // filled by orchestrator (file paths/test IDs)
}
]
}
```
Decision: adopt Hybrid default; allow Markdownonly input and autonormalize.
## Decisions
- Adopt Hybrid acceptance criteria schema by default; normalize Markdown to structured items with stable IDs `AC-<taskId>-<n>`.
- Use conventional commits plus footers and a unified trailer `Refs: TM-<tag>-<id>[.<sub>]` across PRDs for robust mapping.

View File

@@ -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>

View File

@@ -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"

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

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

View File

@@ -60,19 +60,6 @@ The following documentation is also available in the `docs` directory:
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
#### Claude Code Quick Install
For Claude Code users:
```bash
claude mcp add taskmaster-ai -- npx -y task-master-ai
```
Don't forget to add your API keys to the configuration:
- in the root .env of your Project
- in the "env" section of your mcp config for taskmaster-ai
## Requirements
Taskmaster utilizes AI across several commands, and those require a separate API key. You can use a variety of models from different AI providers provided you add your API keys. For example, if you want to use Claude 3.7, you'll need an Anthropic API key.
@@ -105,18 +92,17 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
| | Project | `<project_folder>/.cursor/mcp.json` | `<project_folder>\.cursor\mcp.json` | `mcpServers` |
| **Windsurf** | Global | `~/.codeium/windsurf/mcp_config.json` | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` | `mcpServers` |
| **VS Code** | Project | `<project_folder>/.vscode/mcp.json` | `<project_folder>\.vscode\mcp.json` | `servers` |
| **Q CLI** | Global | `~/.aws/amazonq/mcp.json` | | `mcpServers` |
##### Manual Configuration
###### Cursor & Windsurf & Q Developer CLI (`mcpServers`)
###### Cursor & Windsurf (`mcpServers`)
```json
{
"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",
@@ -136,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`.
###### VSCode (`servers` + `type`)
@@ -145,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",

View File

@@ -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

View File

@@ -1,39 +1,43 @@
{
"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",
"types": "./dist/index.d.ts",
"exports": {
".": "./src/index.ts"
".": {
"types": "./src/index.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"files": ["dist", "README.md"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"lint": "biome check src",
"format": "biome format --write src",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:unit": "vitest run -t unit",
"test:integration": "vitest run -t integration",
"test:e2e": "vitest run --dir tests/e2e",
"test:ci": "vitest run --coverage --reporter=dot"
"test:watch": "vitest"
},
"dependencies": {
"@tm/core": "*",
"boxen": "^8.0.1",
"chalk": "5.6.2",
"@tm/workflow-engine": "*",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"cli-table3": "^0.6.5",
"commander": "^12.1.0",
"inquirer": "^12.5.0",
"ora": "^8.2.0"
"inquirer": "^9.2.10",
"open": "^10.2.0",
"ora": "^8.1.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/inquirer": "^9.0.3",
"@types/node": "^22.10.5",
"tsup": "^8.3.0",
"tsx": "^4.20.4",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
@@ -43,10 +47,5 @@
},
"keywords": ["task-master", "cli", "task-management", "productivity"],
"author": "",
"license": "MIT",
"typesVersions": {
"*": {
"*": ["src/*"]
}
}
"license": "MIT"
}

View File

@@ -494,17 +494,6 @@ export class AuthCommand extends Command {
/**
* 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 authCommand = new AuthCommand();
program.addCommand(authCommand);
return authCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): AuthCommand {
const authCommand = new AuthCommand(name);

View File

@@ -1,713 +0,0 @@
/**
* @fileoverview Context command for managing org/brief selection
* Provides a clean interface for workspace context management
*/
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora, { Ora } from 'ora';
import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import * as ui from '../utils/ui.js';
/**
* Result type from context command
*/
export interface ContextResult {
success: boolean;
action: 'show' | 'select-org' | 'select-brief' | 'clear' | 'set';
context?: UserContext;
message?: string;
}
/**
* ContextCommand extending Commander's Command class
* Manages user's workspace context (org/brief selection)
*/
export class ContextCommand extends Command {
private authManager: AuthManager;
private lastResult?: ContextResult;
constructor(name?: string) {
super(name || 'context');
// Initialize auth manager
this.authManager = AuthManager.getInstance();
// Configure the command
this.description(
'Manage workspace context (organization and brief selection)'
);
// Add subcommands
this.addOrgCommand();
this.addBriefCommand();
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;
}
await this.executeShow();
});
}
/**
* Add org selection subcommand
*/
private addOrgCommand(): void {
this.command('org')
.description('Select an organization')
.action(async () => {
await this.executeSelectOrg();
});
}
/**
* Add brief selection subcommand
*/
private addBriefCommand(): void {
this.command('brief')
.description('Select a brief within the current organization')
.action(async () => {
await this.executeSelectBrief();
});
}
/**
* Add clear subcommand
*/
private addClearCommand(): void {
this.command('clear')
.description('Clear all context selections')
.action(async () => {
await this.executeClear();
});
}
/**
* Add set subcommand for direct context setting
*/
private addSetCommand(): void {
this.command('set')
.description('Set context directly')
.option('--org <id>', 'Organization ID')
.option('--org-name <name>', 'Organization name')
.option('--brief <id>', 'Brief ID')
.option('--brief-name <name>', 'Brief name')
.action(async (options) => {
await this.executeSet(options);
});
}
/**
* Execute show current context
*/
private async executeShow(): Promise<void> {
try {
const result = this.displayContext();
this.setLastResult(result);
} catch (error: any) {
this.handleError(error);
process.exit(1);
}
}
/**
* Display current context
*/
private displayContext(): ContextResult {
// Check authentication first
if (!this.authManager.isAuthenticated()) {
console.log(chalk.yellow('✗ Not authenticated'));
console.log(chalk.gray('\n Run "tm auth login" to authenticate first'));
return {
success: false,
action: 'show',
message: 'Not authenticated'
};
}
const context = this.authManager.getContext();
console.log(chalk.cyan('\n🌍 Workspace Context\n'));
if (context && (context.orgId || context.briefId)) {
if (context.orgName || context.orgId) {
console.log(chalk.green('✓ Organization'));
if (context.orgName) {
console.log(chalk.white(` ${context.orgName}`));
}
if (context.orgId) {
console.log(chalk.gray(` ID: ${context.orgId}`));
}
}
if (context.briefName || context.briefId) {
console.log(chalk.green('\n✓ Brief'));
if (context.briefName) {
console.log(chalk.white(` ${context.briefName}`));
}
if (context.briefId) {
console.log(chalk.gray(` ID: ${context.briefId}`));
}
}
if (context.updatedAt) {
console.log(
chalk.gray(
`\n Last updated: ${new Date(context.updatedAt).toLocaleString()}`
)
);
}
return {
success: true,
action: 'show',
context,
message: 'Context loaded'
};
} else {
console.log(chalk.yellow('✗ No context selected'));
console.log(
chalk.gray('\n Run "tm context org" to select an organization')
);
console.log(chalk.gray(' Run "tm context brief" to select a brief'));
return {
success: true,
action: 'show',
message: 'No context selected'
};
}
}
/**
* Execute org selection
*/
private async executeSelectOrg(): Promise<void> {
try {
// Check authentication
if (!this.authManager.isAuthenticated()) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
const result = await this.selectOrganization();
this.setLastResult(result);
if (!result.success) {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
}
}
/**
* Select an organization interactively
*/
private async selectOrganization(): Promise<ContextResult> {
const spinner = ora('Fetching organizations...').start();
try {
// Fetch organizations from API
const organizations = await this.authManager.getOrganizations();
spinner.stop();
if (organizations.length === 0) {
ui.displayWarning('No organizations available');
return {
success: false,
action: 'select-org',
message: 'No organizations available'
};
}
// Prompt for selection
const { selectedOrg } = await inquirer.prompt([
{
type: 'list',
name: 'selectedOrg',
message: 'Select an organization:',
choices: organizations.map((org) => ({
name: org.name,
value: org
}))
}
]);
// Update context
await this.authManager.updateContext({
orgId: selectedOrg.id,
orgName: selectedOrg.name,
// Clear brief when changing org
briefId: undefined,
briefName: undefined
});
ui.displaySuccess(`Selected organization: ${selectedOrg.name}`);
return {
success: true,
action: 'select-org',
context: this.authManager.getContext() || undefined,
message: `Selected organization: ${selectedOrg.name}`
};
} catch (error) {
spinner.fail('Failed to fetch organizations');
throw error;
}
}
/**
* Execute brief selection
*/
private async executeSelectBrief(): Promise<void> {
try {
// Check authentication
if (!this.authManager.isAuthenticated()) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
// Check if org is selected
const context = this.authManager.getContext();
if (!context?.orgId) {
ui.displayError(
'No organization selected. Run "tm context org" first.'
);
process.exit(1);
}
const result = await this.selectBrief(context.orgId);
this.setLastResult(result);
if (!result.success) {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
}
}
/**
* Select a brief within the current organization
*/
private async selectBrief(orgId: string): Promise<ContextResult> {
const spinner = ora('Fetching briefs...').start();
try {
// Fetch briefs from API
const briefs = await this.authManager.getBriefs(orgId);
spinner.stop();
if (briefs.length === 0) {
ui.displayWarning('No briefs available in this organization');
return {
success: false,
action: 'select-brief',
message: 'No briefs available'
};
}
// Prompt for selection
const { selectedBrief } = await inquirer.prompt([
{
type: 'list',
name: 'selectedBrief',
message: 'Select a brief:',
choices: [
{ name: '(No brief - organization level)', value: null },
...briefs.map((brief) => ({
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
value: brief
}))
]
}
]);
if (selectedBrief) {
// Update context with brief
const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`;
await this.authManager.updateContext({
briefId: selectedBrief.id,
briefName: briefName
});
ui.displaySuccess(`Selected brief: ${briefName}`);
return {
success: true,
action: 'select-brief',
context: this.authManager.getContext() || undefined,
message: `Selected brief: ${selectedBrief.name}`
};
} else {
// Clear brief selection
await this.authManager.updateContext({
briefId: undefined,
briefName: undefined
});
ui.displaySuccess('Cleared brief selection (organization level)');
return {
success: true,
action: 'select-brief',
context: this.authManager.getContext() || undefined,
message: 'Cleared brief selection'
};
}
} catch (error) {
spinner.fail('Failed to fetch briefs');
throw error;
}
}
/**
* Execute clear context
*/
private async executeClear(): Promise<void> {
try {
// Check authentication
if (!this.authManager.isAuthenticated()) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
const result = await this.clearContext();
this.setLastResult(result);
if (!result.success) {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
}
}
/**
* Clear all context selections
*/
private async clearContext(): Promise<ContextResult> {
try {
await this.authManager.clearContext();
ui.displaySuccess('Context cleared');
return {
success: true,
action: 'clear',
message: 'Context cleared'
};
} catch (error) {
ui.displayError(`Failed to clear context: ${(error as Error).message}`);
return {
success: false,
action: 'clear',
message: `Failed to clear context: ${(error as Error).message}`
};
}
}
/**
* Execute set context with options
*/
private async executeSet(options: any): Promise<void> {
try {
// Check authentication
if (!this.authManager.isAuthenticated()) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
const result = await this.setContext(options);
this.setLastResult(result);
if (!result.success) {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
}
}
/**
* 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 schemeless)
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
*/
private async setContext(options: any): Promise<ContextResult> {
try {
const context: Partial<UserContext> = {};
if (options.org) {
context.orgId = options.org;
}
if (options.orgName) {
context.orgName = options.orgName;
}
if (options.brief) {
context.briefId = options.brief;
}
if (options.briefName) {
context.briefName = options.briefName;
}
if (Object.keys(context).length === 0) {
ui.displayWarning('No context options provided');
return {
success: false,
action: 'set',
message: 'No context options provided'
};
}
await this.authManager.updateContext(context);
ui.displaySuccess('Context updated');
// Display what was set
if (context.orgName || context.orgId) {
console.log(
chalk.gray(` Organization: ${context.orgName || context.orgId}`)
);
}
if (context.briefName || context.briefId) {
console.log(
chalk.gray(` Brief: ${context.briefName || context.briefId}`)
);
}
return {
success: true,
action: 'set',
context: this.authManager.getContext() || undefined,
message: 'Context updated'
};
} catch (error) {
ui.displayError(`Failed to set context: ${(error as Error).message}`);
return {
success: false,
action: 'set',
message: `Failed to set context: ${(error as Error).message}`
};
}
}
/**
* Handle errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
console.error(chalk.red(`\n✗ ${error.message}`));
if (error.code === 'NOT_AUTHENTICATED') {
ui.displayWarning('Please authenticate first: tm auth login');
}
} else {
const msg = error?.message ?? String(error);
console.error(chalk.red(`Error: ${msg}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
}
}
/**
* Set the last result for programmatic access
*/
private setLastResult(result: ContextResult): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): ContextResult | undefined {
return this.lastResult;
}
/**
* Get current context (for programmatic usage)
*/
getContext(): UserContext | null {
return this.authManager.getContext();
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
// No resources to clean up for context command
}
/**
* Static method to register this command on an existing program
*/
static registerOn(program: Command): Command {
const contextCommand = new ContextCommand();
program.addCommand(contextCommand);
return contextCommand;
}
/**
* Alternative registration that returns the command for chaining
*/
static register(program: Command, name?: string): ContextCommand {
const contextCommand = new ContextCommand(name);
program.addCommand(contextCommand);
return contextCommand;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Command registry - exports all CLI commands for central registration
*/
import type { Command } from 'commander';
import { ListTasksCommand } from './list.command.js';
import { AuthCommand } from './auth.command.js';
import WorkflowCommand from './workflow.command.js';
// Define interface for command classes that can register themselves
export interface CommandRegistrar {
register(program: Command, name?: string): any;
}
// Future commands can be added here as they're created
// The pattern is: each command exports a class with a static register(program: Command, name?: string) method
/**
* Auto-register all exported commands that implement the CommandRegistrar interface
*/
export function registerAllCommands(program: Command): void {
// Get all exports from this module
const commands = [
ListTasksCommand,
AuthCommand,
WorkflowCommand
// Add new commands here as they're imported above
];
commands.forEach((CommandClass) => {
if (
'register' in CommandClass &&
typeof CommandClass.register === 'function'
) {
CommandClass.register(program);
}
});
}

View File

@@ -15,20 +15,7 @@ import {
STATUS_ICONS,
type OutputFormat
} from '@tm/core';
import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js';
import {
displayHeader,
displayDashboards,
calculateTaskStatistics,
calculateSubtaskStatistics,
calculateDependencyStatistics,
getPriorityBreakdown,
displayRecommendedNextTask,
getTaskDescription,
displaySuggestedNextSteps,
type NextTaskInfo
} from '../ui/index.js';
/**
* Options interface for the list command
@@ -50,7 +37,7 @@ export interface ListTasksResult {
total: number;
filtered: number;
tag?: string;
storageType: Exclude<StorageType, 'auto'>;
storageType: 'file' | 'api';
}
/**
@@ -257,16 +244,19 @@ export class ListTasksCommand extends Command {
* Display in text format with tables
*/
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
const { tasks, tag } = data;
const { tasks, total, filtered, tag, storageType } = data;
// Get file path for display
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
// Header
ui.displayBanner(`Task List${tag ? ` (${tag})` : ''}`);
// Display header without banner (banner already shown by main CLI)
displayHeader({
tag: tag || 'master',
filePath: filePath
});
// Statistics
console.log(chalk.blue.bold('\n📊 Statistics:\n'));
console.log(` Total tasks: ${chalk.cyan(total)}`);
console.log(` Filtered: ${chalk.cyan(filtered)}`);
if (tag) {
console.log(` Tag: ${chalk.cyan(tag)}`);
}
console.log(` Storage: ${chalk.cyan(storageType)}`);
// No tasks message
if (tasks.length === 0) {
@@ -274,50 +264,21 @@ export class ListTasksCommand extends Command {
return;
}
// Calculate statistics
const taskStats = calculateTaskStatistics(tasks);
const subtaskStats = calculateSubtaskStatistics(tasks);
const depStats = calculateDependencyStatistics(tasks);
const priorityBreakdown = getPriorityBreakdown(tasks);
// Find next task following the same logic as findNextTask
const nextTask = this.findNextTask(tasks);
// Display dashboard boxes
displayDashboards(
taskStats,
subtaskStats,
priorityBreakdown,
depStats,
nextTask
);
// Task table - no title, just show the table directly
// Task table
console.log(chalk.blue.bold(`\n📋 Tasks (${tasks.length}):\n`));
console.log(
ui.createTaskTable(tasks, {
showSubtasks: withSubtasks,
showDependencies: true,
showComplexity: true // Enable complexity column
showDependencies: true
})
);
// Display recommended next task section immediately after table
if (nextTask) {
// Find the full task object to get description
const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id));
const description = fullTask ? getTaskDescription(fullTask) : undefined;
displayRecommendedNextTask({
...nextTask,
status: 'pending', // Next task is typically pending
description
});
} else {
displayRecommendedNextTask(undefined);
}
// Display suggested next steps at the end
displaySuggestedNextSteps();
// Progress bar
const completedCount = tasks.filter(
(t: Task) => t.status === 'done'
).length;
console.log(chalk.blue.bold('\n📊 Overall Progress:\n'));
console.log(` ${ui.createProgressBar(completedCount, tasks.length)}`);
}
/**
@@ -327,128 +288,6 @@ export class ListTasksCommand extends Command {
this.lastResult = result;
}
/**
* Find the next task to work on
* Implements the same logic as scripts/modules/task-manager/find-next-task.js
*/
private findNextTask(tasks: Task[]): NextTaskInfo | undefined {
const priorityValues: Record<string, number> = {
critical: 4,
high: 3,
medium: 2,
low: 1
};
// Build set of completed task IDs (including subtasks)
const completedIds = new Set<string>();
tasks.forEach((t) => {
if (t.status === 'done' || t.status === 'completed') {
completedIds.add(String(t.id));
}
if (t.subtasks) {
t.subtasks.forEach((st) => {
if (st.status === 'done' || st.status === 'completed') {
completedIds.add(`${t.id}.${st.id}`);
}
});
}
});
// First, look for eligible subtasks in in-progress parent tasks
const candidateSubtasks: NextTaskInfo[] = [];
tasks
.filter(
(t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0
)
.forEach((parent) => {
parent.subtasks!.forEach((st) => {
const stStatus = (st.status || 'pending').toLowerCase();
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
// Check if dependencies are satisfied
const fullDeps =
st.dependencies?.map((d) => {
// Handle both numeric and string IDs
if (typeof d === 'string' && d.includes('.')) {
return d;
}
return `${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}`,
priority: st.priority || parent.priority || 'medium',
dependencies: fullDeps.map((d) => String(d))
});
}
});
});
if (candidateSubtasks.length > 0) {
// Sort by priority, then by dependencies count, then by ID
candidateSubtasks.sort((a, b) => {
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
const depCountA = a.dependencies?.length || 0;
const depCountB = b.dependencies?.length || 0;
if (depCountA !== depCountB) return depCountA - depCountB;
return String(a.id).localeCompare(String(b.id));
});
return candidateSubtasks[0];
}
// Fall back to finding eligible top-level tasks
const eligibleTasks = tasks.filter((task) => {
// Skip non-eligible statuses
const status = (task.status || 'pending').toLowerCase();
if (status !== 'pending' && status !== 'in-progress') return false;
// Check dependencies
const deps = task.dependencies || [];
const depsSatisfied =
deps.length === 0 ||
deps.every((depId) => completedIds.has(String(depId)));
return depsSatisfied;
});
if (eligibleTasks.length === 0) return undefined;
// Sort eligible tasks
eligibleTasks.sort((a, b) => {
// Priority (higher first)
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
// Dependencies count (fewer first)
const depCountA = a.dependencies?.length || 0;
const depCountB = b.dependencies?.length || 0;
if (depCountA !== depCountB) return depCountA - depCountB;
// ID (lower first)
return Number(a.id) - Number(b.id);
});
const nextTask = eligibleTasks[0];
return {
id: nextTask.id,
title: nextTask.title,
priority: nextTask.priority,
dependencies: nextTask.dependencies?.map((d) => String(d))
};
}
/**
* Get the last result (for programmatic usage)
*/
@@ -468,17 +307,6 @@ export class ListTasksCommand extends Command {
/**
* 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 listCommand = new ListTasksCommand();
program.addCommand(listCommand);
return listCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): ListTasksCommand {
const listCommand = new ListTasksCommand(name);

View File

@@ -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();
}

View File

@@ -1,343 +0,0 @@
/**
* @fileoverview ShowCommand 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 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';
/**
* Options interface for the show command
*/
export interface ShowCommandOptions {
id?: string;
status?: string;
format?: 'text' | 'json';
silent?: boolean;
project?: string;
}
/**
* Result type from show command
*/
export interface ShowTaskResult {
task: Task | null;
found: boolean;
storageType: Exclude<StorageType, 'auto'>;
}
/**
* Result type for multiple tasks
*/
export interface ShowMultipleTasksResult {
tasks: Task[];
notFound: string[];
storageType: Exclude<StorageType, 'auto'>;
}
/**
* ShowCommand extending Commander's Command class
* This is a thin presentation layer over @tm/core
*/
export class ShowCommand extends Command {
private tmCore?: TaskMasterCore;
private lastResult?: ShowTaskResult | ShowMultipleTasksResult;
constructor(name?: string) {
super(name || 'show');
// Configure the command
this.description('Display detailed information about one or more tasks')
.argument('[id]', 'Task ID(s) to show (comma-separated for multiple)')
.option(
'-i, --id <id>',
'Task ID(s) to show (comma-separated for multiple)'
)
.option('-s, --status <status>', 'Filter subtasks by status')
.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 (taskId: string | undefined, options: ShowCommandOptions) => {
await this.executeCommand(taskId, options);
}
);
}
/**
* Execute the show command
*/
private async executeCommand(
taskId: string | undefined,
options: ShowCommandOptions
): Promise<void> {
try {
// Validate options
if (!this.validateOptions(options)) {
process.exit(1);
}
// Initialize tm-core
await this.initializeCore(options.project || process.cwd());
// Get the task ID from argument or option
const idArg = taskId || options.id;
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
process.exit(1);
}
// Check if multiple IDs are provided (comma-separated)
const taskIds = idArg
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0);
// Get tasks from core
const result =
taskIds.length > 1
? await this.getMultipleTasks(taskIds, options)
: await this.getSingleTask(taskIds[0], options);
// Store result for programmatic access
this.setLastResult(result);
// Display results
if (!options.silent) {
this.displayResults(result, options);
}
} catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
/**
* Validate command options
*/
private validateOptions(options: ShowCommandOptions): 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 a single task from tm-core
*/
private async getSingleTask(
taskId: string,
_options: ShowCommandOptions
): Promise<ShowTaskResult> {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
// Get the task
const task = await this.tmCore.getTask(taskId);
// Get storage type
const storageType = this.tmCore.getStorageType();
return {
task,
found: task !== null,
storageType: storageType as Exclude<StorageType, 'auto'>
};
}
/**
* Get multiple tasks from tm-core
*/
private async getMultipleTasks(
taskIds: string[],
_options: ShowCommandOptions
): Promise<ShowMultipleTasksResult> {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
const tasks: Task[] = [];
const notFound: string[] = [];
// Get each task individually
for (const taskId of taskIds) {
const task = await this.tmCore.getTask(taskId);
if (task) {
tasks.push(task);
} else {
notFound.push(taskId);
}
}
// Get storage type
const storageType = this.tmCore.getStorageType();
return {
tasks,
notFound,
storageType: storageType as Exclude<StorageType, 'auto'>
};
}
/**
* Display results based on format
*/
private displayResults(
result: ShowTaskResult | ShowMultipleTasksResult,
options: ShowCommandOptions
): void {
const format = options.format || 'text';
switch (format) {
case 'json':
this.displayJson(result);
break;
case 'text':
default:
if ('task' in result) {
// Single task result
this.displaySingleTask(result, options);
} else {
// Multiple tasks result
this.displayMultipleTasks(result, options);
}
break;
}
}
/**
* Display in JSON format
*/
private displayJson(result: ShowTaskResult | ShowMultipleTasksResult): void {
console.log(JSON.stringify(result, null, 2));
}
/**
* Display a single task in text format
*/
private displaySingleTask(
result: ShowTaskResult,
options: ShowCommandOptions
): 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;
}
// Use the global task details display function
displayTaskDetails(result.task, {
statusFilter: options.status,
showSuggestedActions: true
});
}
/**
* Display multiple tasks in text format
*/
private displayMultipleTasks(
result: ShowMultipleTasksResult,
_options: ShowCommandOptions
): void {
// Header
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
if (result.notFound.length > 0) {
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
}
if (result.tasks.length === 0) {
ui.displayWarning('No tasks found matching the criteria.');
return;
}
// Task table
console.log(chalk.blue.bold(`\n📋 Tasks:\n`));
console.log(
ui.createTaskTable(result.tasks, {
showSubtasks: true,
showDependencies: true
})
);
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**
* Set the last result for programmatic access
*/
private setLastResult(
result: ShowTaskResult | ShowMultipleTasksResult
): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): ShowTaskResult | ShowMultipleTasksResult | 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
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const showCommand = new ShowCommand();
program.addCommand(showCommand);
return showCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): ShowCommand {
const showCommand = new ShowCommand(name);
program.addCommand(showCommand);
return showCommand;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview Workflow Command
* Main workflow command with subcommands
*/
import { Command } from 'commander';
import {
WorkflowStartCommand,
WorkflowListCommand,
WorkflowStopCommand,
WorkflowStatusCommand
} from './workflow/index.js';
/**
* WorkflowCommand - Main workflow command with subcommands
*/
export class WorkflowCommand extends Command {
constructor(name?: string) {
super(name || 'workflow');
this.description('Manage task execution workflows with git worktrees and Claude Code')
.alias('wf');
// Register subcommands
this.addSubcommands();
}
private addSubcommands(): void {
// Start workflow
WorkflowStartCommand.register(this);
// List workflows
WorkflowListCommand.register(this);
// Stop workflow
WorkflowStopCommand.register(this);
// Show workflow status
WorkflowStatusCommand.register(this);
// Alias commands for convenience
this.addCommand(new WorkflowStartCommand('run')); // tm workflow run <task-id>
this.addCommand(new WorkflowStopCommand('kill')); // tm workflow kill <workflow-id>
this.addCommand(new WorkflowStatusCommand('info')); // tm workflow info <workflow-id>
}
/**
* Static method to register this command on an existing program
*/
static register(program: Command, name?: string): WorkflowCommand {
const workflowCommand = new WorkflowCommand(name);
program.addCommand(workflowCommand);
return workflowCommand;
}
}
export default WorkflowCommand;

View File

@@ -0,0 +1,9 @@
/**
* @fileoverview Workflow Commands
* Exports for all workflow-related CLI commands
*/
export * from './workflow-start.command.js';
export * from './workflow-list.command.js';
export * from './workflow-stop.command.js';
export * from './workflow-status.command.js';

View File

@@ -0,0 +1,253 @@
/**
* @fileoverview Workflow List Command
* List active and recent workflow executions
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig,
type WorkflowExecutionContext
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowListOptions {
project?: string;
status?: string;
format?: 'text' | 'json' | 'compact';
worktreeBase?: string;
claude?: string;
all?: boolean;
}
/**
* WorkflowListCommand - List workflow executions
*/
export class WorkflowListCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'list');
this.description('List active and recent workflow executions')
.alias('ls')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('-s, --status <status>', 'Filter by status (running, completed, failed, etc.)')
.option('-f, --format <format>', 'Output format (text, json, compact)', 'text')
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('--all', 'Show all workflows including completed ones')
.action(async (options: WorkflowListOptions) => {
await this.executeCommand(options);
});
}
private async executeCommand(options: WorkflowListOptions): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
// Get workflows
let workflows = this.workflowManager!.listWorkflows();
// Apply status filter
if (options.status) {
workflows = workflows.filter(w => w.status === options.status);
}
// Apply active filter (default behavior)
if (!options.all) {
workflows = workflows.filter(w =>
['pending', 'initializing', 'running', 'paused'].includes(w.status)
);
}
// Display results
this.displayResults(workflows, options);
} catch (error: any) {
ui.displayError(error.message || 'Failed to list workflows');
process.exit(1);
}
}
private async initializeWorkflowManager(options: WorkflowListOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private displayResults(workflows: WorkflowExecutionContext[], options: WorkflowListOptions): void {
switch (options.format) {
case 'json':
this.displayJson(workflows);
break;
case 'compact':
this.displayCompact(workflows);
break;
case 'text':
default:
this.displayText(workflows);
break;
}
}
private displayJson(workflows: WorkflowExecutionContext[]): void {
console.log(JSON.stringify({
workflows: workflows.map(w => ({
workflowId: `workflow-${w.taskId}`,
taskId: w.taskId,
taskTitle: w.taskTitle,
status: w.status,
worktreePath: w.worktreePath,
branchName: w.branchName,
processId: w.processId,
startedAt: w.startedAt,
lastActivity: w.lastActivity,
metadata: w.metadata
})),
total: workflows.length,
timestamp: new Date().toISOString()
}, null, 2));
}
private displayCompact(workflows: WorkflowExecutionContext[]): void {
if (workflows.length === 0) {
console.log(chalk.gray('No workflows found'));
return;
}
workflows.forEach(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
const statusDisplay = this.getStatusDisplay(workflow.status);
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
console.log(
`${chalk.cyan(workflowId)} ${statusDisplay} ${workflow.taskTitle} ${chalk.gray(`(${duration})`)}`
);
});
}
private displayText(workflows: WorkflowExecutionContext[]): void {
ui.displayBanner('Active Workflows');
if (workflows.length === 0) {
ui.displayWarning('No workflows found');
console.log();
console.log(chalk.blue('💡 Start a new workflow with:'));
console.log(` ${chalk.cyan('tm workflow start <task-id>')}`);
return;
}
// Statistics
console.log(chalk.blue.bold('\n📊 Statistics:\n'));
const statusCounts = this.getStatusCounts(workflows);
Object.entries(statusCounts).forEach(([status, count]) => {
console.log(` ${this.getStatusDisplay(status)}: ${chalk.cyan(count)}`);
});
// Workflows table
console.log(chalk.blue.bold(`\n🔄 Workflows (${workflows.length}):\n`));
const tableData = workflows.map(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
return [
chalk.cyan(workflowId),
chalk.yellow(workflow.taskId),
workflow.taskTitle.substring(0, 30) + (workflow.taskTitle.length > 30 ? '...' : ''),
this.getStatusDisplay(workflow.status),
workflow.processId ? chalk.green(workflow.processId.toString()) : chalk.gray('N/A'),
chalk.gray(duration),
chalk.gray(path.basename(workflow.worktreePath))
];
});
console.log(ui.createTable(
['Workflow ID', 'Task ID', 'Task Title', 'Status', 'PID', 'Duration', 'Worktree'],
tableData
));
// Running workflows actions
const runningWorkflows = workflows.filter(w => w.status === 'running');
if (runningWorkflows.length > 0) {
console.log(chalk.blue.bold('\n🚀 Quick Actions:\n'));
runningWorkflows.slice(0, 3).forEach(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
console.log(` • Attach to ${chalk.cyan(workflowId)}: ${chalk.gray(`tm workflow attach ${workflowId}`)}`);
});
if (runningWorkflows.length > 3) {
console.log(` ${chalk.gray(`... and ${runningWorkflows.length - 3} more`)}`);
}
}
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.orange },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || { icon: '❓', color: chalk.white };
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
private getStatusCounts(workflows: WorkflowExecutionContext[]): Record<string, number> {
const counts: Record<string, number> = {};
workflows.forEach(workflow => {
counts[workflow.status] = (counts[workflow.status] || 0) + 1;
});
return counts;
}
private formatDuration(start: Date, end: Date): string {
const diff = end.getTime() - start.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else {
return '<1m';
}
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowListCommand {
const command = new WorkflowListCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -0,0 +1,239 @@
/**
* @fileoverview Workflow Start Command
* Start task execution in isolated worktree with Claude Code process
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
createTaskMasterCore,
type TaskMasterCore
} from '@tm/core';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStartOptions {
project?: string;
branch?: string;
timeout?: number;
worktreeBase?: string;
claude?: string;
debug?: boolean;
env?: string;
}
/**
* WorkflowStartCommand - Start task execution workflow
*/
export class WorkflowStartCommand extends Command {
private tmCore?: TaskMasterCore;
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'start');
this.description('Start task execution in isolated worktree')
.argument('<task-id>', 'Task ID to execute')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('-b, --branch <name>', 'Custom branch name for worktree')
.option('-t, --timeout <minutes>', 'Execution timeout in minutes', '60')
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('--debug', 'Enable debug logging')
.option('--env <vars>', 'Environment variables (KEY=VALUE,KEY2=VALUE2)')
.action(async (taskId: string, options: WorkflowStartOptions) => {
await this.executeCommand(taskId, options);
});
}
private async executeCommand(taskId: string, options: WorkflowStartOptions): Promise<void> {
try {
// Initialize components
await this.initializeCore(options.project || process.cwd());
await this.initializeWorkflowManager(options);
// Get task details
const task = await this.getTask(taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
// Check if task already has active workflow
const existingWorkflow = this.workflowManager!.getWorkflowByTaskId(taskId);
if (existingWorkflow) {
ui.displayWarning(`Task ${taskId} already has an active workflow`);
console.log(`Workflow ID: ${chalk.cyan('workflow-' + taskId)}`);
console.log(`Status: ${this.getStatusDisplay(existingWorkflow.status)}`);
console.log(`Worktree: ${chalk.gray(existingWorkflow.worktreePath)}`);
return;
}
// Parse environment variables
const env = this.parseEnvironmentVariables(options.env);
// Display task info
ui.displayBanner(`Starting Workflow for Task ${taskId}`);
console.log(`${chalk.blue('Task:')} ${task.title}`);
console.log(`${chalk.blue('Description:')} ${task.description}`);
if (task.dependencies?.length) {
console.log(`${chalk.blue('Dependencies:')} ${task.dependencies.join(', ')}`);
}
console.log(`${chalk.blue('Priority:')} ${task.priority || 'normal'}`);
console.log();
// Start workflow
ui.displaySpinner('Creating worktree and starting Claude Code process...');
const workflowId = await this.workflowManager!.startTaskExecution(task, {
branchName: options.branch,
timeout: parseInt(options.timeout || '60'),
env
});
const workflow = this.workflowManager!.getWorkflowStatus(workflowId);
ui.displaySuccess('Workflow started successfully!');
console.log();
console.log(`${chalk.green('✓')} Workflow ID: ${chalk.cyan(workflowId)}`);
console.log(`${chalk.green('✓')} Worktree: ${chalk.gray(workflow?.worktreePath)}`);
console.log(`${chalk.green('✓')} Branch: ${chalk.gray(workflow?.branchName)}`);
console.log(`${chalk.green('✓')} Process ID: ${chalk.gray(workflow?.processId)}`);
console.log();
// Display next steps
console.log(chalk.blue.bold('📋 Next Steps:'));
console.log(` • Monitor: ${chalk.cyan(`tm workflow status ${workflowId}`)}`);
console.log(` • Attach: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
console.log();
// Setup event listeners for real-time updates
this.setupEventListeners();
} catch (error: any) {
ui.displayError(error.message || 'Failed to start workflow');
if (options.debug && error.stack) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
}
}
private async initializeWorkflowManager(options: WorkflowStartOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: parseInt(options.timeout || '60'),
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: options.debug || false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async getTask(taskId: string) {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
const result = await this.tmCore.getTaskList({});
return result.tasks.find(task => task.id === taskId);
}
private parseEnvironmentVariables(envString?: string): Record<string, string> | undefined {
if (!envString) return undefined;
const env: Record<string, string> = {};
for (const pair of envString.split(',')) {
const [key, ...valueParts] = pair.trim().split('=');
if (key && valueParts.length > 0) {
env[key] = valueParts.join('=');
}
}
return Object.keys(env).length > 0 ? env : undefined;
}
private getStatusDisplay(status: string): string {
const colors = {
pending: chalk.yellow,
initializing: chalk.blue,
running: chalk.green,
paused: chalk.orange,
completed: chalk.green,
failed: chalk.red,
cancelled: chalk.gray,
timeout: chalk.red
};
const color = colors[status as keyof typeof colors] || chalk.white;
return color(status);
}
private setupEventListeners(): void {
if (!this.workflowManager) return;
this.workflowManager.on('workflow.started', (event) => {
console.log(`${chalk.green('🚀')} Workflow started: ${event.workflowId}`);
});
this.workflowManager.on('process.output', (event) => {
if (event.data?.stream === 'stdout') {
console.log(`${chalk.blue('[OUT]')} ${event.data.data.trim()}`);
} else if (event.data?.stream === 'stderr') {
console.log(`${chalk.red('[ERR]')} ${event.data.data.trim()}`);
}
});
this.workflowManager.on('workflow.completed', (event) => {
console.log(`${chalk.green('✅')} Workflow completed: ${event.workflowId}`);
});
this.workflowManager.on('workflow.failed', (event) => {
console.log(`${chalk.red('❌')} Workflow failed: ${event.workflowId}`);
if (event.error) {
console.log(`${chalk.red('Error:')} ${event.error.message}`);
}
});
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
// Don't cleanup workflows, just disconnect
this.workflowManager.removeAllListeners();
}
if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined;
}
}
static register(program: Command, name?: string): WorkflowStartCommand {
const command = new WorkflowStartCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -0,0 +1,339 @@
/**
* @fileoverview Workflow Status Command
* Show detailed status of a specific workflow
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStatusOptions {
project?: string;
worktreeBase?: string;
claude?: string;
watch?: boolean;
format?: 'text' | 'json';
}
/**
* WorkflowStatusCommand - Show workflow execution status
*/
export class WorkflowStatusCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'status');
this.description('Show detailed status of a workflow execution')
.argument('<workflow-id>', 'Workflow ID or task ID to check')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('-w, --watch', 'Watch for status changes (refresh every 2 seconds)')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.action(async (workflowId: string, options: WorkflowStatusOptions) => {
await this.executeCommand(workflowId, options);
});
}
private async executeCommand(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
if (options.watch) {
await this.watchWorkflowStatus(workflowId, options);
} else {
await this.showWorkflowStatus(workflowId, options);
}
} catch (error: any) {
ui.displayError(error.message || 'Failed to get workflow status');
process.exit(1);
}
}
private async initializeWorkflowManager(options: WorkflowStatusOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async showWorkflowStatus(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
// Try to find workflow by ID or task ID
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
// Try as task ID
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}
if (options.format === 'json') {
this.displayJsonStatus(workflow);
} else {
this.displayTextStatus(workflow);
}
}
private async watchWorkflowStatus(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
console.log(chalk.blue.bold('👀 Watching workflow status (Press Ctrl+C to exit)\n'));
let lastStatus = '';
let updateCount = 0;
const updateStatus = async () => {
try {
// Clear screen and move cursor to top
if (updateCount > 0) {
process.stdout.write('\x1b[2J\x1b[0f');
}
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
console.log(chalk.red(`Workflow not found: ${workflowId}`));
return;
}
// Display header with timestamp
console.log(chalk.blue.bold('👀 Watching Workflow Status'));
console.log(chalk.gray(`Last updated: ${new Date().toLocaleTimeString()}\n`));
this.displayTextStatus(workflow);
// Check if workflow has ended
if (['completed', 'failed', 'cancelled', 'timeout'].includes(workflow.status)) {
console.log(chalk.yellow('\n⚠ Workflow has ended. Stopping watch mode.'));
return;
}
updateCount++;
} catch (error) {
console.error(chalk.red('Error updating status:'), error);
}
};
// Initial display
await updateStatus();
// Setup interval for updates
const interval = setInterval(updateStatus, 2000);
// Handle Ctrl+C
process.on('SIGINT', () => {
clearInterval(interval);
console.log(chalk.yellow('\n👋 Stopped watching workflow status'));
process.exit(0);
});
// Keep the process alive
await new Promise(() => {});
}
private displayJsonStatus(workflow: any): void {
const status = {
workflowId: `workflow-${workflow.taskId}`,
taskId: workflow.taskId,
taskTitle: workflow.taskTitle,
taskDescription: workflow.taskDescription,
status: workflow.status,
worktreePath: workflow.worktreePath,
branchName: workflow.branchName,
processId: workflow.processId,
startedAt: workflow.startedAt,
lastActivity: workflow.lastActivity,
duration: this.calculateDuration(workflow.startedAt, workflow.lastActivity),
metadata: workflow.metadata
};
console.log(JSON.stringify(status, null, 2));
}
private displayTextStatus(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
ui.displayBanner(`Workflow Status: ${workflowId}`);
// Basic information
console.log(chalk.blue.bold('\n📋 Basic Information:\n'));
console.log(` Workflow ID: ${chalk.cyan(workflowId)}`);
console.log(` Task ID: ${chalk.cyan(workflow.taskId)}`);
console.log(` Task Title: ${workflow.taskTitle}`);
console.log(` Status: ${this.getStatusDisplay(workflow.status)}`);
console.log(` Duration: ${chalk.gray(duration)}`);
// Task details
if (workflow.taskDescription) {
console.log(chalk.blue.bold('\n📝 Task Details:\n'));
console.log(` ${workflow.taskDescription}`);
}
// Process information
console.log(chalk.blue.bold('\n⚙ Process Information:\n'));
console.log(` Process ID: ${workflow.processId ? chalk.green(workflow.processId) : chalk.gray('N/A')}`);
console.log(` Worktree: ${chalk.gray(workflow.worktreePath)}`);
console.log(` Branch: ${chalk.gray(workflow.branchName)}`);
// Timing information
console.log(chalk.blue.bold('\n⏰ Timing:\n'));
console.log(` Started: ${chalk.gray(workflow.startedAt.toLocaleString())}`);
console.log(` Last Activity: ${chalk.gray(workflow.lastActivity.toLocaleString())}`);
// Metadata
if (workflow.metadata && Object.keys(workflow.metadata).length > 0) {
console.log(chalk.blue.bold('\n🔖 Metadata:\n'));
Object.entries(workflow.metadata).forEach(([key, value]) => {
console.log(` ${key}: ${chalk.gray(String(value))}`);
});
}
// Status-specific information
this.displayStatusSpecificInfo(workflow);
// Actions
this.displayAvailableActions(workflow);
}
private displayStatusSpecificInfo(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
switch (workflow.status) {
case 'running':
console.log(chalk.blue.bold('\n🚀 Running Status:\n'));
console.log(` ${chalk.green('●')} Process is actively executing`);
console.log(` ${chalk.blue('')} Monitor output with: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
break;
case 'paused':
console.log(chalk.blue.bold('\n⏸ Paused Status:\n'));
console.log(` ${chalk.yellow('●')} Workflow is paused`);
console.log(` ${chalk.blue('')} Resume with: ${chalk.cyan(`tm workflow resume ${workflowId}`)}`);
break;
case 'completed':
console.log(chalk.blue.bold('\n✅ Completed Status:\n'));
console.log(` ${chalk.green('●')} Workflow completed successfully`);
console.log(` ${chalk.blue('')} Resources have been cleaned up`);
break;
case 'failed':
console.log(chalk.blue.bold('\n❌ Failed Status:\n'));
console.log(` ${chalk.red('●')} Workflow execution failed`);
console.log(` ${chalk.blue('')} Check logs for error details`);
break;
case 'initializing':
console.log(chalk.blue.bold('\n🔄 Initializing Status:\n'));
console.log(` ${chalk.blue('●')} Setting up worktree and process`);
console.log(` ${chalk.blue('')} This should complete shortly`);
break;
}
}
private displayAvailableActions(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
console.log(chalk.blue.bold('\n🎯 Available Actions:\n'));
switch (workflow.status) {
case 'running':
console.log(` • Attach: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
console.log(` • Pause: ${chalk.cyan(`tm workflow pause ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'paused':
console.log(` • Resume: ${chalk.cyan(`tm workflow resume ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'pending':
case 'initializing':
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'completed':
case 'failed':
case 'cancelled':
console.log(` • View logs: ${chalk.cyan(`tm workflow logs ${workflowId}`)}`);
console.log(` • Start new: ${chalk.cyan(`tm workflow start ${workflow.taskId}`)}`);
break;
}
console.log(` • List all: ${chalk.cyan('tm workflow list')}`);
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.orange },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || { icon: '❓', color: chalk.white };
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
private formatDuration(start: Date, end: Date): string {
const diff = end.getTime() - start.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`;
} else {
return `${seconds}s`;
}
}
private calculateDuration(start: Date, end: Date): number {
return Math.floor((end.getTime() - start.getTime()) / 1000);
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowStatusCommand {
const command = new WorkflowStatusCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -0,0 +1,260 @@
/**
* @fileoverview Workflow Stop Command
* Stop and clean up workflow execution
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStopOptions {
project?: string;
worktreeBase?: string;
claude?: string;
force?: boolean;
all?: boolean;
}
/**
* WorkflowStopCommand - Stop workflow execution
*/
export class WorkflowStopCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'stop');
this.description('Stop workflow execution and clean up resources')
.argument('[workflow-id]', 'Workflow ID to stop (or task ID)')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option(
'--worktree-base <path>',
'Base directory for worktrees',
'../task-worktrees'
)
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('-f, --force', 'Force stop (kill process immediately)')
.option('--all', 'Stop all running workflows')
.action(
async (
workflowId: string | undefined,
options: WorkflowStopOptions
) => {
await this.executeCommand(workflowId, options);
}
);
}
private async executeCommand(
workflowId: string | undefined,
options: WorkflowStopOptions
): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
if (options.all) {
await this.stopAllWorkflows(options);
} else if (workflowId) {
await this.stopSingleWorkflow(workflowId, options);
} else {
ui.displayError('Please specify a workflow ID or use --all flag');
process.exit(1);
}
} catch (error: any) {
ui.displayError(error.message || 'Failed to stop workflow');
process.exit(1);
}
}
private async initializeWorkflowManager(
options: WorkflowStopOptions
): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(
projectRoot,
options.worktreeBase || '../task-worktrees'
);
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async stopSingleWorkflow(
workflowId: string,
options: WorkflowStopOptions
): Promise<void> {
// Try to find workflow by ID or task ID
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
// Try as task ID
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const actualWorkflowId = `workflow-${workflow.taskId}`;
// Display workflow info
console.log(chalk.blue.bold(`🛑 Stopping Workflow: ${actualWorkflowId}`));
console.log(`${chalk.blue('Task:')} ${workflow.taskTitle}`);
console.log(
`${chalk.blue('Status:')} ${this.getStatusDisplay(workflow.status)}`
);
console.log(
`${chalk.blue('Worktree:')} ${chalk.gray(workflow.worktreePath)}`
);
if (workflow.processId) {
console.log(
`${chalk.blue('Process ID:')} ${chalk.gray(workflow.processId)}`
);
}
console.log();
// Confirm if not forced
if (!options.force && ['running', 'paused'].includes(workflow.status)) {
const shouldProceed = await ui.confirm(
`Are you sure you want to stop this ${workflow.status} workflow?`
);
if (!shouldProceed) {
console.log(chalk.gray('Operation cancelled'));
return;
}
}
// Stop the workflow
ui.displaySpinner('Stopping workflow and cleaning up resources...');
await this.workflowManager!.stopTaskExecution(
actualWorkflowId,
options.force
);
ui.displaySuccess('Workflow stopped successfully!');
console.log();
console.log(`${chalk.green('✓')} Process terminated`);
console.log(`${chalk.green('✓')} Worktree cleaned up`);
console.log(`${chalk.green('✓')} State updated`);
}
private async stopAllWorkflows(options: WorkflowStopOptions): Promise<void> {
const workflows = this.workflowManager!.listWorkflows();
const activeWorkflows = workflows.filter((w) =>
['pending', 'initializing', 'running', 'paused'].includes(w.status)
);
if (activeWorkflows.length === 0) {
ui.displayWarning('No active workflows to stop');
return;
}
console.log(
chalk.blue.bold(`🛑 Stopping ${activeWorkflows.length} Active Workflows`)
);
console.log();
// List workflows to be stopped
activeWorkflows.forEach((workflow) => {
console.log(
`${chalk.cyan(`workflow-${workflow.taskId}`)} - ${workflow.taskTitle} ${this.getStatusDisplay(workflow.status)}`
);
});
console.log();
// Confirm if not forced
if (!options.force) {
const shouldProceed = await ui.confirm(
`Are you sure you want to stop all ${activeWorkflows.length} active workflows?`
);
if (!shouldProceed) {
console.log(chalk.gray('Operation cancelled'));
return;
}
}
// Stop all workflows
ui.displaySpinner('Stopping all workflows...');
let stopped = 0;
let failed = 0;
for (const workflow of activeWorkflows) {
try {
const workflowId = `workflow-${workflow.taskId}`;
await this.workflowManager!.stopTaskExecution(
workflowId,
options.force
);
stopped++;
} catch (error) {
console.error(
`${chalk.red('✗')} Failed to stop workflow ${workflow.taskId}: ${error}`
);
failed++;
}
}
console.log();
if (stopped > 0) {
ui.displaySuccess(`Successfully stopped ${stopped} workflows`);
}
if (failed > 0) {
ui.displayWarning(`Failed to stop ${failed} workflows`);
}
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.hex('#FFA500') },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || {
icon: '❓',
color: chalk.white
};
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowStopCommand {
const command = new WorkflowStopCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -5,23 +5,15 @@
// Commands
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';
export { WorkflowCommand } from './commands/workflow.command.js';
// Command registry
export { registerAllCommands } from './commands/index.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,

View File

@@ -1,567 +0,0 @@
/**
* @fileoverview Dashboard components for Task Master CLI
* Displays project statistics and dependency information
*/
import chalk from 'chalk';
import boxen from 'boxen';
import type { Task, TaskPriority } from '@tm/core/types';
/**
* Statistics for task collection
*/
export interface TaskStatistics {
total: number;
done: number;
inProgress: number;
pending: number;
blocked: number;
deferred: number;
cancelled: number;
review?: number;
completionPercentage: number;
}
/**
* Statistics for dependencies
*/
export interface DependencyStatistics {
tasksWithNoDeps: number;
tasksReadyToWork: number;
tasksBlockedByDeps: number;
mostDependedOnTaskId?: number;
mostDependedOnCount?: number;
avgDependenciesPerTask: number;
}
/**
* Next task information
*/
export interface NextTaskInfo {
id: string | number;
title: string;
priority?: TaskPriority;
dependencies?: (string | number)[];
complexity?: number | string;
}
/**
* Status breakdown for progress bars
*/
export interface StatusBreakdown {
'in-progress'?: number;
pending?: number;
blocked?: number;
deferred?: number;
cancelled?: number;
review?: number;
}
/**
* Create a progress bar with color-coded status segments
*/
function createProgressBar(
completionPercentage: number,
width: number = 30,
statusBreakdown?: StatusBreakdown
): string {
// If no breakdown provided, use simple green bar
if (!statusBreakdown) {
const filled = Math.round((completionPercentage / 100) * width);
const empty = width - filled;
return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
}
// Build the bar with different colored sections
// Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
let bar = '';
let charsUsed = 0;
// 1. Green filled blocks for completed tasks (done)
const completedChars = Math.round((completionPercentage / 100) * width);
if (completedChars > 0) {
bar += chalk.green('█').repeat(completedChars);
charsUsed += completedChars;
}
// 2. Gray filled blocks for cancelled (won't be done)
if (statusBreakdown.cancelled && charsUsed < width) {
const cancelledChars = Math.round(
(statusBreakdown.cancelled / 100) * width
);
const actualChars = Math.min(cancelledChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.gray('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 3. Gray filled blocks for deferred (won't be done now)
if (statusBreakdown.deferred && charsUsed < width) {
const deferredChars = Math.round((statusBreakdown.deferred / 100) * width);
const actualChars = Math.min(deferredChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.gray('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 4. Blue filled blocks for in-progress (actively working)
if (statusBreakdown['in-progress'] && charsUsed < width) {
const inProgressChars = Math.round(
(statusBreakdown['in-progress'] / 100) * width
);
const actualChars = Math.min(inProgressChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.blue('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 5. Magenta empty blocks for review (almost done)
if (statusBreakdown.review && charsUsed < width) {
const reviewChars = Math.round((statusBreakdown.review / 100) * width);
const actualChars = Math.min(reviewChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.magenta('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// 6. Yellow empty blocks for pending (ready to start)
if (statusBreakdown.pending && charsUsed < width) {
const pendingChars = Math.round((statusBreakdown.pending / 100) * width);
const actualChars = Math.min(pendingChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.yellow('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// 7. Red empty blocks for blocked (can't start yet)
if (statusBreakdown.blocked && charsUsed < width) {
const blockedChars = Math.round((statusBreakdown.blocked / 100) * width);
const actualChars = Math.min(blockedChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.red('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// Fill any remaining space with gray empty yellow blocks
if (charsUsed < width) {
bar += chalk.yellow('░').repeat(width - charsUsed);
}
return bar;
}
/**
* Calculate task statistics from a list of tasks
*/
export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
const stats: TaskStatistics = {
total: tasks.length,
done: 0,
inProgress: 0,
pending: 0,
blocked: 0,
deferred: 0,
cancelled: 0,
review: 0,
completionPercentage: 0
};
tasks.forEach((task) => {
switch (task.status) {
case 'done':
stats.done++;
break;
case 'in-progress':
stats.inProgress++;
break;
case 'pending':
stats.pending++;
break;
case 'blocked':
stats.blocked++;
break;
case 'deferred':
stats.deferred++;
break;
case 'cancelled':
stats.cancelled++;
break;
case 'review':
stats.review = (stats.review || 0) + 1;
break;
}
});
stats.completionPercentage =
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
return stats;
}
/**
* Calculate subtask statistics from tasks
*/
export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
const stats: TaskStatistics = {
total: 0,
done: 0,
inProgress: 0,
pending: 0,
blocked: 0,
deferred: 0,
cancelled: 0,
review: 0,
completionPercentage: 0
};
tasks.forEach((task) => {
if (task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
stats.total++;
switch (subtask.status) {
case 'done':
stats.done++;
break;
case 'in-progress':
stats.inProgress++;
break;
case 'pending':
stats.pending++;
break;
case 'blocked':
stats.blocked++;
break;
case 'deferred':
stats.deferred++;
break;
case 'cancelled':
stats.cancelled++;
break;
case 'review':
stats.review = (stats.review || 0) + 1;
break;
}
});
}
});
stats.completionPercentage =
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
return stats;
}
/**
* Calculate dependency statistics
*/
export function calculateDependencyStatistics(
tasks: Task[]
): DependencyStatistics {
const completedTaskIds = new Set(
tasks.filter((t) => t.status === 'done').map((t) => t.id)
);
const tasksWithNoDeps = tasks.filter(
(t) =>
t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0)
).length;
const tasksWithAllDepsSatisfied = tasks.filter(
(t) =>
t.status !== 'done' &&
t.dependencies &&
t.dependencies.length > 0 &&
t.dependencies.every((depId) => completedTaskIds.has(depId))
).length;
const tasksBlockedByDeps = tasks.filter(
(t) =>
t.status !== 'done' &&
t.dependencies &&
t.dependencies.length > 0 &&
!t.dependencies.every((depId) => completedTaskIds.has(depId))
).length;
// Calculate most depended-on task
const dependencyCount: Record<string, number> = {};
tasks.forEach((task) => {
if (task.dependencies && task.dependencies.length > 0) {
task.dependencies.forEach((depId) => {
const key = String(depId);
dependencyCount[key] = (dependencyCount[key] || 0) + 1;
});
}
});
let mostDependedOnTaskId: number | undefined;
let mostDependedOnCount = 0;
for (const [taskId, count] of Object.entries(dependencyCount)) {
if (count > mostDependedOnCount) {
mostDependedOnCount = count;
mostDependedOnTaskId = parseInt(taskId);
}
}
// Calculate average dependencies
const totalDependencies = tasks.reduce(
(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
0
);
const avgDependenciesPerTask =
tasks.length > 0 ? totalDependencies / tasks.length : 0;
return {
tasksWithNoDeps,
tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied,
tasksBlockedByDeps,
mostDependedOnTaskId,
mostDependedOnCount,
avgDependenciesPerTask
};
}
/**
* Get priority counts
*/
export function getPriorityBreakdown(
tasks: Task[]
): Record<TaskPriority, number> {
const breakdown: Record<TaskPriority, number> = {
critical: 0,
high: 0,
medium: 0,
low: 0
};
tasks.forEach((task) => {
const priority = task.priority || 'medium';
breakdown[priority]++;
});
return breakdown;
}
/**
* Calculate status breakdown as percentages
*/
function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
if (stats.total === 0) return {};
return {
'in-progress': (stats.inProgress / stats.total) * 100,
pending: (stats.pending / stats.total) * 100,
blocked: (stats.blocked / stats.total) * 100,
deferred: (stats.deferred / stats.total) * 100,
cancelled: (stats.cancelled / stats.total) * 100,
review: ((stats.review || 0) / stats.total) * 100
};
}
/**
* Format status counts in the correct order with colors
* @param stats - The statistics object containing counts
* @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label)
*/
function formatStatusLine(
stats: TaskStatistics,
isSubtask: boolean = false
): string {
const parts: string[] = [];
// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
if (isSubtask) {
parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
} else {
parts.push(`Done: ${chalk.green(stats.done)}`);
}
parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`);
parts.push(`Deferred: ${chalk.gray(stats.deferred)}`);
// Add line break for second row
const firstLine = parts.join(' ');
parts.length = 0;
parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`);
parts.push(`Review: ${chalk.magenta(stats.review || 0)}`);
parts.push(`Pending: ${chalk.yellow(stats.pending)}`);
parts.push(`Blocked: ${chalk.red(stats.blocked)}`);
const secondLine = parts.join(' ');
return firstLine + '\n' + secondLine;
}
/**
* Display the project dashboard box
*/
export function displayProjectDashboard(
taskStats: TaskStatistics,
subtaskStats: TaskStatistics,
priorityBreakdown: Record<TaskPriority, number>
): string {
// Calculate status breakdowns using the helper function
const taskStatusBreakdown = calculateStatusBreakdown(taskStats);
const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats);
// Create progress bars with the breakdowns
const taskProgressBar = createProgressBar(
taskStats.completionPercentage,
30,
taskStatusBreakdown
);
const subtaskProgressBar = createProgressBar(
subtaskStats.completionPercentage,
30,
subtaskStatusBreakdown
);
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
const content =
chalk.white.bold('Project Dashboard') +
'\n' +
`Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` +
formatStatusLine(taskStats, false) +
'\n\n' +
`Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` +
formatStatusLine(subtaskStats, true) +
'\n\n' +
chalk.cyan.bold('Priority Breakdown:') +
'\n' +
`${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` +
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` +
`${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`;
return content;
}
/**
* Display the dependency dashboard box
*/
export function displayDependencyDashboard(
depStats: DependencyStatistics,
nextTask?: NextTaskInfo
): string {
const content =
chalk.white.bold('Dependency Status & Next Task') +
'\n' +
chalk.cyan.bold('Dependency Metrics:') +
'\n' +
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` +
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` +
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` +
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${
depStats.mostDependedOnTaskId
? chalk.cyan(
`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`
)
: chalk.gray('None')
}\n` +
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` +
chalk.cyan.bold('Next Task to Work On:') +
'\n' +
`ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${
nextTask
? chalk.white.bold(nextTask.title)
: chalk.yellow('No task available')
}\n` +
`Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${
nextTask?.dependencies?.length
? chalk.cyan(nextTask.dependencies.join(', '))
: chalk.gray('None')
}\n` +
`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
return content;
}
/**
* Display dashboard boxes side by side or stacked
*/
export function displayDashboards(
taskStats: TaskStatistics,
subtaskStats: TaskStatistics,
priorityBreakdown: Record<TaskPriority, number>,
depStats: DependencyStatistics,
nextTask?: NextTaskInfo
): void {
const projectDashboardContent = displayProjectDashboard(
taskStats,
subtaskStats,
priorityBreakdown
);
const dependencyDashboardContent = displayDependencyDashboard(
depStats,
nextTask
);
// Get terminal width
const terminalWidth = process.stdout.columns || 80;
const minDashboardWidth = 50;
const minDependencyWidth = 50;
const totalMinWidth = minDashboardWidth + minDependencyWidth + 4;
// If terminal is wide enough, show side by side
if (terminalWidth >= totalMinWidth) {
const halfWidth = Math.floor(terminalWidth / 2);
const boxContentWidth = halfWidth - 4;
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
width: boxContentWidth,
dimBorder: false
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: 'magenta',
borderStyle: 'round',
width: boxContentWidth,
dimBorder: false
});
// Create side-by-side layout
const dashboardLines = dashboardBox.split('\n');
const dependencyLines = dependencyBox.split('\n');
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
const combinedLines = [];
for (let i = 0; i < maxHeight; i++) {
const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
const paddedDashLine = dashLine.padEnd(halfWidth, ' ');
combinedLines.push(paddedDashLine + depLine);
}
console.log(combinedLines.join('\n'));
} else {
// Show stacked vertically
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
});
console.log(dashboardBox);
console.log(dependencyBox);
}
}

View File

@@ -1,45 +0,0 @@
/**
* @fileoverview Task Master header component
* Displays the banner, version, project info, and file path
*/
import chalk from 'chalk';
/**
* Header configuration options
*/
export interface HeaderOptions {
title?: string;
tag?: string;
filePath?: string;
}
/**
* Display the Task Master header with project info
*/
export function displayHeader(options: HeaderOptions = {}): void {
const { filePath, tag } = options;
// Display tag and file path info
if (tag) {
let tagInfo = '';
if (tag && tag !== 'master') {
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
} else {
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
}
console.log(tagInfo);
if (filePath) {
// Convert to absolute path if it's relative
const absolutePath = filePath.startsWith('/')
? filePath
: `${process.cwd()}/${filePath}`;
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
}
console.log(); // Empty line for spacing
}
}

View File

@@ -1,9 +0,0 @@
/**
* @fileoverview UI components exports
*/
export * from './header.component.js';
export * from './dashboard.component.js';
export * from './next-task.component.js';
export * from './suggested-steps.component.js';
export * from './task-detail.component.js';

View File

@@ -1,134 +0,0 @@
/**
* @fileoverview Next task recommendation component
* Displays detailed information about the recommended next task
*/
import chalk from 'chalk';
import boxen from 'boxen';
import type { Task } from '@tm/core/types';
/**
* Next task display options
*/
export interface NextTaskDisplayOptions {
id: string | number;
title: string;
priority?: string;
status?: string;
dependencies?: (string | number)[];
description?: string;
}
/**
* Display the recommended next task section
*/
export function displayRecommendedNextTask(
task: NextTaskDisplayOptions | undefined
): void {
if (!task) {
// If no task available, show a message
console.log(
boxen(
chalk.yellow(
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
),
{
padding: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '⚠ NO TASKS AVAILABLE ⚠',
titleAlignment: 'center'
}
)
);
return;
}
// Build the content for the next task box
const content = [];
// Task header with ID and title
content.push(
`🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}`
);
content.push('');
// Priority and Status line
const statusLine = [];
if (task.priority) {
const priorityColor =
task.priority === 'high'
? chalk.red
: task.priority === 'medium'
? chalk.yellow
: chalk.gray;
statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`);
}
if (task.status) {
const statusDisplay =
task.status === 'pending'
? chalk.yellow('○ pending')
: task.status === 'in-progress'
? chalk.blue('▶ in-progress')
: chalk.gray(task.status);
statusLine.push(`Status: ${statusDisplay}`);
}
content.push(statusLine.join(' '));
// Dependencies
const depsDisplay =
!task.dependencies || task.dependencies.length === 0
? chalk.gray('None')
: chalk.cyan(task.dependencies.join(', '));
content.push(`Dependencies: ${depsDisplay}`);
// Description if available
if (task.description) {
content.push('');
content.push(`Description: ${chalk.white(task.description)}`);
}
// Action commands
content.push('');
content.push(
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}`
);
content.push(
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}`
);
// Display in a styled box with orange border
console.log(
boxen(content.join('\n'), {
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: 'round',
borderColor: '#FFA500', // Orange color
title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'),
titleAlignment: 'center',
width: process.stdout.columns * 0.97,
fullscreen: false
})
);
}
/**
* Get task description from the full task object
*/
export function getTaskDescription(task: Task): string | undefined {
// Try to get description from the task
// This could be from task.description or the first line of task.details
if ('description' in task && task.description) {
return task.description as string;
}
if ('details' in task && task.details) {
// Take first sentence or line from details
const details = task.details as string;
const firstLine = details.split('\n')[0];
const firstSentence = firstLine.split('.')[0];
return firstSentence;
}
return undefined;
}

View File

@@ -1,31 +0,0 @@
/**
* @fileoverview Suggested next steps component
* Displays helpful command suggestions at the end of the list
*/
import chalk from 'chalk';
import boxen from 'boxen';
/**
* Display suggested next steps section
*/
export function displaySuggestedNextSteps(): void {
const steps = [
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next`,
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`
];
console.log(
boxen(
chalk.white.bold('Suggested Next Steps:') + '\n\n' + steps.join('\n'),
{
padding: 1,
margin: { top: 0, bottom: 1 },
borderStyle: 'round',
borderColor: 'gray',
width: process.stdout.columns * 0.97
}
)
);
}

View File

@@ -1,335 +0,0 @@
/**
* @fileoverview Task detail component for show command
* Displays detailed task information in a structured format
*/
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { marked, MarkedExtension } from 'marked';
import { markedTerminal } from 'marked-terminal';
import type { Task } from '@tm/core/types';
import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js';
// Configure marked to use terminal renderer with subtle colors
marked.use(
markedTerminal({
// More subtle colors that match the overall design
code: (code: string) => {
// Custom code block handler to preserve formatting
return code
.split('\n')
.map((line) => ' ' + chalk.cyan(line))
.join('\n');
},
blockquote: chalk.gray.italic,
html: chalk.gray,
heading: chalk.white.bold, // White bold for headings
hr: chalk.gray,
listitem: chalk.white, // White for list items
paragraph: chalk.white, // White for paragraphs (default text color)
strong: chalk.white.bold, // White bold for strong text
em: chalk.white.italic, // White italic for emphasis
codespan: chalk.cyan, // Cyan for inline code (no background)
del: chalk.dim.strikethrough,
link: chalk.blue,
href: chalk.blue.underline,
// Add more explicit code block handling
showSectionPrefix: false,
unescape: true,
emoji: false,
// Try to preserve whitespace in code blocks
tab: 4,
width: 120
}) as MarkedExtension
);
// Also set marked options to preserve whitespace
marked.setOptions({
breaks: true,
gfm: true
});
/**
* Display the task header with tag
*/
export function displayTaskHeader(
taskId: string | number,
title: string
): void {
// Display task header box
console.log(
boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'blue',
borderStyle: 'round'
})
);
}
/**
* Display task properties in a table format
*/
export function displayTaskProperties(task: Task): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Create table for task properties - simple 2-column layout
const table = new Table({
head: [],
style: {
head: [],
border: ['grey']
},
colWidths: [
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.8)
],
wordWrap: true
});
const deps =
task.dependencies && task.dependencies.length > 0
? task.dependencies.map((d) => String(d)).join(', ')
: 'None';
// Build the left column (labels) and right column (values)
const labels = [
chalk.cyan('ID:'),
chalk.cyan('Title:'),
chalk.cyan('Status:'),
chalk.cyan('Priority:'),
chalk.cyan('Dependencies:'),
chalk.cyan('Complexity:'),
chalk.cyan('Description:')
].join('\n');
const values = [
String(task.id),
task.title,
getStatusWithColor(task.status),
getPriorityWithColor(task.priority),
deps,
'N/A',
task.description || ''
].join('\n');
table.push([labels, values]);
console.log(table.toString());
}
/**
* Display implementation details in a box
*/
export function displayImplementationDetails(details: string): void {
// Handle all escaped characters properly
const cleanDetails = details
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\t/g, '\t') // Convert \t to actual tabs
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Parse markdown to terminal-friendly format
const markdownResult = marked(cleanDetails);
const formattedDetails =
typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
console.log(
boxen(
chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
{
padding: 1,
borderStyle: 'round',
borderColor: 'cyan', // Changed to cyan to match the original
width: terminalWidth // Fixed width to match the original
}
)
);
}
/**
* Display test strategy in a box
*/
export function displayTestStrategy(testStrategy: string): void {
// Handle all escaped characters properly (same as implementation details)
const cleanStrategy = testStrategy
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\t/g, '\t') // Convert \t to actual tabs
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Parse markdown to terminal-friendly format (same as implementation details)
const markdownResult = marked(cleanStrategy);
const formattedStrategy =
typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
console.log(
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
padding: 1,
borderStyle: 'round',
borderColor: 'cyan', // Changed to cyan to match implementation details
width: terminalWidth
})
);
}
/**
* Display subtasks in a table format
*/
export function displaySubtasks(
subtasks: Array<{
id: string | number;
title: string;
status: any;
description?: string;
dependencies?: string[];
}>,
parentId: string | number
): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Display subtasks header
console.log(
boxen(chalk.magenta.bold('Subtasks'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 1, bottom: 0 }
})
);
// Create subtasks table
const table = new Table({
head: [
chalk.magenta.bold('ID'),
chalk.magenta.bold('Status'),
chalk.magenta.bold('Title'),
chalk.magenta.bold('Deps')
],
style: {
head: [],
border: ['grey']
},
colWidths: [
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.6),
Math.floor(terminalWidth * 0.15)
],
wordWrap: true
});
subtasks.forEach((subtask) => {
const subtaskId = `${parentId}.${subtask.id}`;
// Format dependencies
const deps =
subtask.dependencies && subtask.dependencies.length > 0
? subtask.dependencies.join(', ')
: 'None';
table.push([
subtaskId,
getStatusWithColor(subtask.status),
subtask.title,
deps
]);
});
console.log(table.toString());
}
/**
* Display suggested actions
*/
export function displaySuggestedActions(taskId: string | number): void {
console.log(
boxen(
chalk.white.bold('Suggested Actions:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
{
padding: 1,
margin: { top: 1 },
borderStyle: 'round',
borderColor: 'green',
width: process.stdout.columns * 0.95 || 100
}
)
);
}
/**
* 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);
}
}

View File

@@ -1,9 +0,0 @@
/**
* @fileoverview Main UI exports
*/
// Export all components
export * from './components/index.js';
// Re-export existing UI utilities
export * from '../utils/ui.js';

View File

@@ -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);
});
});
}

View File

@@ -6,7 +6,7 @@
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import type { Task, TaskStatus, TaskPriority } from '@tm/core/types';
import type { Task, TaskStatus, TaskPriority } from '@tm/core';
/**
* Get colored status display with ASCII icons (matches scripts/modules/ui.js style)
@@ -18,44 +18,19 @@ export function getStatusWithColor(
const statusConfig = {
done: {
color: chalk.green,
icon: '✓',
tableIcon: '✓'
},
pending: {
color: chalk.yellow,
icon: '○',
tableIcon: '○'
},
icon: String.fromCharCode(8730),
tableIcon: String.fromCharCode(8730)
}, // √
pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' },
'in-progress': {
color: chalk.hex('#FFA500'),
icon: '▶',
tableIcon: ''
},
deferred: {
color: chalk.gray,
icon: 'x',
tableIcon: 'x'
},
review: {
color: chalk.magenta,
icon: '?',
tableIcon: '?'
},
cancelled: {
color: chalk.gray,
icon: 'x',
tableIcon: 'x'
},
blocked: {
color: chalk.red,
icon: '!',
tableIcon: '!'
},
completed: {
color: chalk.green,
icon: '✓',
tableIcon: '✓'
}
icon: String.fromCharCode(9654),
tableIcon: '>'
}, // ▶
deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' },
blocked: { color: chalk.red, icon: '!', tableIcon: '!' },
review: { color: chalk.magenta, icon: '?', tableIcon: '?' },
cancelled: { color: chalk.gray, icon: 'X', tableIcon: 'X' }
};
const config = statusConfig[status] || {
@@ -64,7 +39,18 @@ export function getStatusWithColor(
tableIcon: 'X'
};
const icon = forTable ? config.tableIcon : config.icon;
// Use simple ASCII characters for stable display
const simpleIcons = {
done: String.fromCharCode(8730), // √
pending: 'o',
'in-progress': '>',
deferred: 'x',
blocked: '!',
review: '?',
cancelled: 'X'
};
const icon = forTable ? simpleIcons[status] || 'X' : config.icon;
return config.color(`${icon} ${status}`);
}
@@ -259,24 +245,10 @@ export function createTaskTable(
} = options || {};
// Calculate dynamic column widths based on terminal width
const terminalWidth = process.stdout.columns * 0.9 || 100;
// Adjust column widths to better match the original layout
const terminalWidth = process.stdout.columns || 100;
const baseColWidths = showComplexity
? [
Math.floor(terminalWidth * 0.06),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.12)
] // ID, Title, Status, Priority, Dependencies, Complexity
: [
Math.floor(terminalWidth * 0.08),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.18),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2)
]; // ID, Title, Status, Priority, Dependencies
? [8, Math.floor(terminalWidth * 0.35), 18, 12, 15, 12] // ID, Title, Status, Priority, Dependencies, Complexity
: [8, Math.floor(terminalWidth * 0.4), 18, 12, 20]; // ID, Title, Status, Priority, Dependencies
const headers = [
chalk.blue.bold('ID'),
@@ -312,19 +284,11 @@ export function createTaskTable(
];
if (showDependencies) {
// For table display, show simple format without status icons
if (!task.dependencies || task.dependencies.length === 0) {
row.push(chalk.gray('None'));
} else {
row.push(
chalk.cyan(task.dependencies.map((d) => String(d)).join(', '))
);
}
row.push(formatDependenciesWithStatus(task.dependencies, tasks));
}
if (showComplexity) {
// Show N/A if no complexity score
row.push(chalk.gray('N/A'));
if (showComplexity && 'complexity' in task) {
row.push(getComplexityWithColor(task.complexity as number | string));
}
table.push(row);
@@ -360,3 +324,61 @@ export function createTaskTable(
return table.toString();
}
/**
* Display a spinner with message (mock implementation)
*/
export function displaySpinner(message: string): void {
console.log(chalk.blue('◐'), chalk.gray(message));
}
/**
* Simple confirmation prompt
*/
export async function confirm(message: string): Promise<boolean> {
// For now, return true. In a real implementation, use inquirer
console.log(chalk.yellow('?'), chalk.white(message), chalk.gray('(y/n)'));
// Mock implementation - in production this would use inquirer
return new Promise((resolve) => {
process.stdin.once('data', (data) => {
const answer = data.toString().trim().toLowerCase();
resolve(answer === 'y' || answer === 'yes');
});
process.stdin.resume();
});
}
/**
* Create a generic table
*/
export function createTable(headers: string[], rows: string[][]): string {
const table = new Table({
head: headers.map(h => chalk.blue.bold(h)),
style: {
head: [],
border: ['gray']
},
chars: {
'top': '─',
'top-mid': '┬',
'top-left': '┌',
'top-right': '┐',
'bottom': '─',
'bottom-mid': '┴',
'bottom-left': '└',
'bottom-right': '┘',
'left': '│',
'left-mid': '├',
'mid': '─',
'mid-mid': '┼',
'right': '│',
'right-mid': '┤',
'middle': '│'
}
});
rows.forEach(row => table.push(row));
return table.toString();
}

View File

@@ -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"]
}

15
apps/cli/tsup.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: 'node18',
splitting: false,
sourcemap: true,
clean: true,
dts: true,
shims: true,
esbuildOptions(options) {
options.platform = 'node';
}
});

View File

@@ -1,9 +1,3 @@
# docs
## 0.0.4
## 0.0.3
## 0.0.2
## 0.0.1

View File

@@ -1,24 +1,22 @@
# Task Master Documentation
Welcome to the Task Master documentation. This documentation site provides comprehensive guides for getting started with Task Master.
Welcome to the Task Master documentation. Use the links below to navigate to the information you need:
## Getting Started
- [Quick Start Guide](/getting-started/quick-start) - Complete setup and first-time usage guide
- [Requirements](/getting-started/quick-start/requirements) - What you need to get started
- [Installation](/getting-started/quick-start/installation) - How to install Task Master
- [Configuration Guide](archive/configuration.md) - Set up environment variables and customize Task Master
- [Tutorial](archive/ctutorial.md) - Step-by-step guide to getting started with Task Master
## Core Capabilities
## Reference
- [MCP Tools](/capabilities/mcp) - Model Control Protocol integration
- [CLI Commands](/capabilities/cli-root-commands) - Command line interface reference
- [Task Structure](/capabilities/task-structure) - Understanding tasks and subtasks
- [Command Reference](archive/ccommand-reference.md) - Complete list of all available commands
- [Task Structure](archive/ctask-structure.md) - Understanding the task format and features
## Best Practices
## Examples & Licensing
- [Advanced Configuration](/best-practices/configuration-advanced) - Detailed configuration options
- [Advanced Tasks](/best-practices/advanced-tasks) - Working with complex task structures
- [Example Interactions](archive/cexamples.md) - Common Cursor AI interaction examples
- [Licensing Information](archive/clicensing.md) - Detailed information about the license
## Need More Help?
If you can't find what you're looking for in these docs, please check the root README.md or visit our [GitHub repository](https://github.com/eyaltoledano/claude-task-master).
If you can't find what you're looking for in these docs, please check the [main README](../README.md) or visit our [GitHub repository](https://github.com/eyaltoledano/claude-task-master).

View File

@@ -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",

View File

@@ -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">

View File

@@ -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.

View File

@@ -156,7 +156,7 @@ sidebarTitle: "CLI Commands"
# Use an alternative tasks file
task-master analyze-complexity --file=custom-tasks.json
# Use your configured research model for research-backed complexity analysis
# Use Perplexity AI for research-backed complexity analysis
task-master analyze-complexity --research
```
</Accordion>

View File

@@ -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",
@@ -108,5 +108,5 @@ You dont need to configure everything up front. Most settings can be left as
</Accordion>
<Note>
For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/best-practices/configuration-advanced) page.
For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/docs/best-practices/configuration-advanced) page.
</Note>

View File

@@ -56,4 +56,4 @@ If you ran into problems and had to debug errors you can create new rules as you
By now you have all you need to get started executing code faster and smarter with Task Master.
If you have any questions please check out [Frequently Asked Questions](/getting-started/faq)
If you have any questions please check out [Frequently Asked Questions](/docs/getting-started/faq)

View File

@@ -30,19 +30,6 @@ cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21
```
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
### Claude Code Quick Install
For Claude Code users:
```bash
claude mcp add taskmaster-ai -- npx -y task-master-ai
```
Don't forget to add your API keys to the configuration:
- in the root .env of your Project
- in the "env" section of your mcp config for taskmaster-ai
</Accordion>
## Installation Options
@@ -69,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",
@@ -88,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`)
@@ -97,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",

View File

@@ -6,13 +6,13 @@ sidebarTitle: "Quick Start"
This guide is for new users who want to start using Task Master with minimal setup time.
It covers:
- [Requirements](/getting-started/quick-start/requirements): You will need Node.js and an AI model API Key.
- [Installation](/getting-started/quick-start/installation): How to Install Task Master.
- [Configuration](/getting-started/quick-start/configuration-quick): Setting up your API Key, MCP, and more.
- [PRD](/getting-started/quick-start/prd-quick): Writing and parsing your first PRD.
- [Task Setup](/getting-started/quick-start/tasks-quick): Preparing your tasks for execution.
- [Executing Tasks](/getting-started/quick-start/execute-quick): Using Task Master to execute tasks.
- [Rules & Context](/getting-started/quick-start/rules-quick): Learn how and why to build context in your project over time.
- [Requirements](/docs/getting-started/quick-start/requirements): You will need Node.js and an AI model API Key.
- [Installation](/docs/getting-started/quick-start/installation): How to Install Task Master.
- [Configuration](/docs/getting-started/quick-start/configuration-quick): Setting up your API Key, MCP, and more.
- [PRD](/docs/getting-started/quick-start/prd-quick): Writing and parsing your first PRD.
- [Task Setup](/docs/getting-started/quick-start/tasks-quick): Preparing your tasks for execution.
- [Executing Tasks](/docs/getting-started/quick-start/execute-quick): Using Task Master to execute tasks.
- [Rules & Context](/docs/getting-started/quick-start/rules-quick): Learn how and why to build context in your project over time.
<Tip>
By the end of this guide, you'll have everything you need to begin working productively with Task Master.

View File

@@ -61,25 +61,9 @@ Task Master can provide a complexity report which can be helpful to read before
Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further?
```
The agent will use the `analyze_project_complexity` MCP tool, or you can run it directly with the CLI command:
```bash
task-master analyze-complexity
```
For more comprehensive analysis using your configured research model, you can use:
```bash
task-master analyze-complexity --research
```
<Tip>
The `--research` flag uses whatever research model you have configured in `.taskmaster/config.json` (configurable via `task-master models --setup`) for research-backed complexity analysis, providing more informed recommendations.
</Tip>
You can view the report in a friendly table using:
```
Can you show me the complexity report in a more readable format?
```
For more detailed CLI options, see the [Analyze Task Complexity](/capabilities/cli-root-commands#analyze-task-complexity) section.
<Check>Now you are ready to begin [executing tasks](/getting-started/quick-start/execute-quick)</Check>
<Check>Now you are ready to begin [executing tasks](/docs/getting-started/quick-start/execute-quick)</Check>

View File

@@ -4,7 +4,7 @@ Welcome to v1 of the Task Master Docs. Expect weekly updates as we expand and re
We've organized the docs into three sections depending on your experience level and goals:
### Getting Started - Jump in to [Quick Start](/getting-started/quick-start)
### Getting Started - Jump in to [Quick Start](/docs/getting-started/quick-start)
Designed for first-time users. Get set up, create your first PRD, and run your first task.
### Best Practices

View File

@@ -1,13 +1,14 @@
{
"name": "docs",
"version": "0.0.4",
"version": "0.0.1",
"private": true,
"description": "Task Master documentation powered by Mintlify",
"scripts": {
"dev": "mintlify dev",
"build": "mintlify build",
"preview": "mintlify preview"
},
"devDependencies": {
"mintlify": "^4.2.111"
"mintlify": "^4.0.0"
}
}

View File

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

View File

@@ -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)

View File

@@ -103,8 +103,8 @@ async function main() {
// This prevents the multiple React instances issue
// Ensure React is resolved from the workspace root to avoid duplicates
alias: {
react: path.resolve(__dirname, '../../node_modules/react'),
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',
@@ -135,8 +135,8 @@ async function main() {
jsxImportSource: 'react',
external: ['*.css'],
alias: {
react: path.resolve(__dirname, '../../node_modules/react'),
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.3",
"version": "0.24.2-rc.1",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -229,7 +229,6 @@
"build": "npm run build:js && npm run build:css",
"build:js": "node ./esbuild.js --production",
"build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
"dev": "npm run watch",
"package": "npm exec node ./package.mjs",
"package:direct": "node ./package.mjs",
"debug:env": "node ./debug-env.mjs",
@@ -240,7 +239,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "0.27.2"
"task-master-ai": "0.26.0-rc.1"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -254,9 +253,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 +270,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",

View File

@@ -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": {

View File

@@ -1,16 +1,16 @@
import type React from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2, Play } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { PriorityBadge } from './PriorityBadge';
import type { TaskMasterTask } from '../../webview/types';
import { useVSCodeContext } from '../../webview/contexts/VSCodeContext';
interface TaskMetadataSidebarProps {
currentTask: TaskMasterTask;
tasks: TaskMasterTask[];
complexity: any;
isSubtask: boolean;
sendMessage: (message: any) => Promise<any>;
onStatusChange: (status: TaskMasterTask['status']) => void;
onDependencyClick: (depId: string) => void;
isRegenerating?: boolean;
@@ -22,17 +22,16 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
tasks,
complexity,
isSubtask,
sendMessage,
onStatusChange,
onDependencyClick,
isRegenerating = false,
isAppending = false
}) => {
const { sendMessage } = useVSCodeContext();
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
const [mcpComplexityScore, setMcpComplexityScore] = useState<
number | undefined
>(undefined);
const [isStartingTask, setIsStartingTask] = useState(false);
// Get complexity score from task
const currentComplexityScore = complexity?.score;
@@ -98,40 +97,6 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
}
};
// Handle starting a task
const handleStartTask = async () => {
if (!currentTask || isStartingTask) {
return;
}
setIsStartingTask(true);
try {
// Send message to extension to open terminal
const result = await sendMessage({
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
setIsStartingTask(false);
}
};
// Effect to handle complexity on task change
useEffect(() => {
if (currentTask?.id) {
@@ -319,30 +284,6 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div className="border-b border-textSeparator-foreground" />
)}
{/* Start Task Button */}
<div className="mt-4">
<Button
onClick={handleStartTask}
variant="default"
size="sm"
className="w-full text-xs"
disabled={
isRegenerating ||
isAppending ||
isStartingTask ||
currentTask?.status === 'done' ||
currentTask?.status === 'in-progress'
}
>
{isStartingTask ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Play className="w-4 h-4 mr-2" />
)}
{isStartingTask ? 'Starting...' : 'Start Task'}
</Button>
</div>
</div>
</div>
</div>

View File

@@ -208,6 +208,7 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
tasks={allTasks}
complexity={complexity}
isSubtask={isSubtask}
sendMessage={sendMessage}
onStatusChange={handleStatusChange}
onDependencyClick={handleDependencyClick}
/>

View File

@@ -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();
}

View File

@@ -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);
}
}
}
}

View File

@@ -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 {
@@ -363,70 +361,6 @@ export class WebviewManager {
}
return;
case 'openTerminal':
// Delegate terminal execution to TerminalManager
const { taskId, taskTitle } = data.data || data; // Handle both nested and direct data
this.logger.log(
`Webview openTerminal - taskId: ${taskId} (type: ${typeof taskId}), taskTitle: ${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
});
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'
);
}
}
} catch (error) {
this.logger.warn(
'Failed to get current tag for terminal execution:',
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:
throw new Error(`Unknown message type: ${type}`);
}

View File

@@ -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

View File

@@ -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"]

View File

@@ -20,8 +20,357 @@
* Main entry point for globally installed package
*/
// Direct imports instead of spawning child processes
import { runCLI } from '../scripts/modules/commands.js';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { createRequire } from 'module';
import { spawn } from 'child_process';
import { Command } from 'commander';
import { displayHelp, displayBanner } from '../scripts/modules/ui.js';
import { registerCommands } from '../scripts/modules/commands.js';
import { detectCamelCaseFlags } from '../scripts/modules/utils.js';
import chalk from 'chalk';
// Simply run the CLI directly
runCLI();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
// Get package information
const packageJson = require('../package.json');
const version = packageJson.version;
// Get paths to script files
const devScriptPath = resolve(__dirname, '../scripts/dev.js');
const initScriptPath = resolve(__dirname, '../scripts/init.js');
// Helper function to run dev.js with arguments
function runDevScript(args) {
// Debug: Show the transformed arguments when DEBUG=1 is set
if (process.env.DEBUG === '1') {
console.error('\nDEBUG - CLI Wrapper Analysis:');
console.error('- Original command: ' + process.argv.join(' '));
console.error('- Transformed args: ' + args.join(' '));
console.error(
'- dev.js will receive: node ' +
devScriptPath +
' ' +
args.join(' ') +
'\n'
);
}
// For testing: If TEST_MODE is set, just print args and exit
if (process.env.TEST_MODE === '1') {
console.log('Would execute:');
console.log(`node ${devScriptPath} ${args.join(' ')}`);
process.exit(0);
return;
}
const child = spawn('node', [devScriptPath, ...args], {
stdio: 'inherit',
cwd: process.cwd()
});
child.on('close', (code) => {
process.exit(code);
});
}
// Helper function to detect camelCase and convert to kebab-case
const toKebabCase = (str) => str.replace(/([A-Z])/g, '-$1').toLowerCase();
/**
* Create a wrapper action that passes the command to dev.js
* @param {string} commandName - The name of the command
* @returns {Function} Wrapper action function
*/
function createDevScriptAction(commandName) {
return (options, cmd) => {
// Check for camelCase flags and error out with helpful message
const camelCaseFlags = detectCamelCaseFlags(process.argv);
// If camelCase flags were found, show error and exit
if (camelCaseFlags.length > 0) {
console.error('\nError: Please use kebab-case for CLI flags:');
camelCaseFlags.forEach((flag) => {
console.error(` Instead of: --${flag.original}`);
console.error(` Use: --${flag.kebabCase}`);
});
console.error(
'\nExample: task-master parse-prd --num-tasks=5 instead of --numTasks=5\n'
);
process.exit(1);
}
// Since we've ensured no camelCase flags, we can now just:
// 1. Start with the command name
const args = [commandName];
// 3. Get positional arguments and explicit flags from the command line
const commandArgs = [];
const positionals = new Set(); // Track positional args we've seen
// Find the command in raw process.argv to extract args
const commandIndex = process.argv.indexOf(commandName);
if (commandIndex !== -1) {
// Process all args after the command name
for (let i = commandIndex + 1; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg.startsWith('--')) {
// It's a flag - pass through as is
commandArgs.push(arg);
// Skip the next arg if this is a flag with a value (not --flag=value format)
if (
!arg.includes('=') &&
i + 1 < process.argv.length &&
!process.argv[i + 1].startsWith('--')
) {
commandArgs.push(process.argv[++i]);
}
} else if (!positionals.has(arg)) {
// It's a positional argument we haven't seen
commandArgs.push(arg);
positionals.add(arg);
}
}
}
// Add all command line args we collected
args.push(...commandArgs);
// 4. Add default options from Commander if not specified on command line
// Track which options we've seen on the command line
const userOptions = new Set();
for (const arg of commandArgs) {
if (arg.startsWith('--')) {
// Extract option name (without -- and value)
const name = arg.split('=')[0].slice(2);
userOptions.add(name);
// Add the kebab-case version too, to prevent duplicates
const kebabName = name.replace(/([A-Z])/g, '-$1').toLowerCase();
userOptions.add(kebabName);
// Add the camelCase version as well
const camelName = kebabName.replace(/-([a-z])/g, (_, letter) =>
letter.toUpperCase()
);
userOptions.add(camelName);
}
}
// Add Commander-provided defaults for options not specified by user
Object.entries(options).forEach(([key, value]) => {
// Debug output to see what keys we're getting
if (process.env.DEBUG === '1') {
console.error(`DEBUG - Processing option: ${key} = ${value}`);
}
// Special case for numTasks > num-tasks (a known problem case)
if (key === 'numTasks') {
if (process.env.DEBUG === '1') {
console.error('DEBUG - Converting numTasks to num-tasks');
}
if (!userOptions.has('num-tasks') && !userOptions.has('numTasks')) {
args.push(`--num-tasks=${value}`);
}
return;
}
// Skip built-in Commander properties and options the user provided
if (
['parent', 'commands', 'options', 'rawArgs'].includes(key) ||
userOptions.has(key)
) {
return;
}
// Also check the kebab-case version of this key
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
if (userOptions.has(kebabKey)) {
return;
}
// Add default values, using kebab-case for the parameter name
if (value !== undefined) {
if (typeof value === 'boolean') {
if (value === true) {
args.push(`--${kebabKey}`);
} else if (value === false && key === 'generate') {
args.push('--skip-generate');
}
} else {
// Always use kebab-case for option names
args.push(`--${kebabKey}=${value}`);
}
}
});
// Special handling for parent parameter (uses -p)
if (options.parent && !args.includes('-p') && !userOptions.has('parent')) {
args.push('-p', options.parent);
}
// Debug output for troubleshooting
if (process.env.DEBUG === '1') {
console.error('DEBUG - Command args:', commandArgs);
console.error('DEBUG - User options:', Array.from(userOptions));
console.error('DEBUG - Commander options:', options);
console.error('DEBUG - Final args:', args);
}
// Run the script with our processed args
runDevScript(args);
};
}
// // Special case for the 'init' command which uses a different script
// function registerInitCommand(program) {
// program
// .command('init')
// .description('Initialize a new project')
// .option('-y, --yes', 'Skip prompts and use default values')
// .option('-n, --name <name>', 'Project name')
// .option('-d, --description <description>', 'Project description')
// .option('-v, --version <version>', 'Project version')
// .option('-a, --author <author>', 'Author name')
// .option('--skip-install', 'Skip installing dependencies')
// .option('--dry-run', 'Show what would be done without making changes')
// .action((options) => {
// // Pass through any options to the init script
// const args = [
// '--yes',
// 'name',
// 'description',
// 'version',
// 'author',
// 'skip-install',
// 'dry-run'
// ]
// .filter((opt) => options[opt])
// .map((opt) => {
// if (opt === 'yes' || opt === 'skip-install' || opt === 'dry-run') {
// return `--${opt}`;
// }
// return `--${opt}=${options[opt]}`;
// });
// const child = spawn('node', [initScriptPath, ...args], {
// stdio: 'inherit',
// cwd: process.cwd()
// });
// child.on('close', (code) => {
// process.exit(code);
// });
// });
// }
// Set up the command-line interface
const program = new Command();
program
.name('task-master')
.description('Claude Task Master CLI')
.version(version)
.addHelpText('afterAll', () => {
// Use the same help display function as dev.js for consistency
displayHelp();
return ''; // Return empty string to prevent commander's default help
});
// Add custom help option to directly call our help display
program.helpOption('-h, --help', 'Display help information');
program.on('--help', () => {
displayHelp();
});
// // Add special case commands
// registerInitCommand(program);
program
.command('dev')
.description('Run the dev.js script')
.action(() => {
const args = process.argv.slice(process.argv.indexOf('dev') + 1);
runDevScript(args);
});
// Use a temporary Command instance to get all command definitions
const tempProgram = new Command();
registerCommands(tempProgram);
// For each command in the temp instance, add a modified version to our actual program
tempProgram.commands.forEach((cmd) => {
if (['dev'].includes(cmd.name())) {
// Skip commands we've already defined specially
return;
}
// Create a new command with the same name and description
const newCmd = program.command(cmd.name()).description(cmd.description());
// Copy all options
cmd.options.forEach((opt) => {
newCmd.option(opt.flags, opt.description, opt.defaultValue);
});
// Set the action to proxy to dev.js
newCmd.action(createDevScriptAction(cmd.name()));
});
// Parse the command line arguments
program.parse(process.argv);
// Add global error handling for unknown commands and options
process.on('uncaughtException', (err) => {
// Check if this is a commander.js unknown option error
if (err.code === 'commander.unknownOption') {
const option = err.message.match(/'([^']+)'/)?.[1];
const commandArg = process.argv.find(
(arg) =>
!arg.startsWith('-') &&
arg !== 'task-master' &&
!arg.includes('/') &&
arg !== 'node'
);
const command = commandArg || 'unknown';
console.error(chalk.red(`Error: Unknown option '${option}'`));
console.error(
chalk.yellow(
`Run 'task-master ${command} --help' to see available options for this command`
)
);
process.exit(1);
}
// Check if this is a commander.js unknown command error
if (err.code === 'commander.unknownCommand') {
const command = err.message.match(/'([^']+)'/)?.[1];
console.error(chalk.red(`Error: Unknown command '${command}'`));
console.error(
chalk.yellow(`Run 'task-master --help' to see available commands`)
);
process.exit(1);
}
// Handle other uncaught exceptions
console.error(chalk.red(`Error: ${err.message}`));
if (process.env.DEBUG === '1') {
console.error(err);
}
process.exit(1);
});
// Show help if no command was provided (just 'task-master' with no args)
if (process.argv.length <= 2) {
displayBanner();
displayHelp();
process.exit(0);
}
// Add exports at the end of the file
export { detectCamelCaseFlags };

View File

@@ -235,60 +235,6 @@ node scripts/init.js
- "MCP provider requires session context" → Ensure running in MCP environment
- See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshooting
### MCP Timeout Configuration
Long-running AI operations in taskmaster-ai can exceed the default 60-second MCP timeout. Operations like `parse_prd`, `expand_task`, `research`, and `analyze_project_complexity` may take 2-5 minutes to complete.
#### Adding Timeout Configuration
Add a `timeout` parameter to your MCP configuration to extend the timeout limit. The timeout configuration works identically across MCP clients including Cursor, Windsurf, and RooCode:
```json
{
"mcpServers": {
"task-master-ai": {
"command": "npx",
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
"timeout": 300,
"env": {
"ANTHROPIC_API_KEY": "your-anthropic-api-key"
}
}
}
}
```
**Configuration Details:**
- **`timeout: 300`** - Sets timeout to 300 seconds (5 minutes)
- **Value range**: 1-3600 seconds (1 second to 1 hour)
- **Recommended**: 300 seconds provides sufficient time for most AI operations
- **Format**: Integer value in seconds (not milliseconds)
#### Automatic Setup
When adding taskmaster rules for supported editors, the timeout configuration is automatically included:
```bash
# Automatically includes timeout configuration
task-master rules add cursor
task-master rules add roo
task-master rules add windsurf
task-master rules add vscode
```
#### Troubleshooting Timeouts
If you're still experiencing timeout errors:
1. **Verify configuration**: Check that `timeout: 300` is present in your MCP config
2. **Restart editor**: Restart your editor after making configuration changes
3. **Increase timeout**: For very complex operations, try `timeout: 600` (10 minutes)
4. **Check API keys**: Ensure required API keys are properly configured
**Expected behavior:**
- **Before fix**: Operations fail after 60 seconds with `MCP request timed out after 60000ms`
- **After fix**: Operations complete successfully within the configured timeout limit
### Google Vertex AI Configuration
Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:

View File

@@ -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",

View File

@@ -1,4 +1,4 @@
# Available Models as of September 23, 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 |
@@ -119,7 +111,7 @@
| groq | deepseek-r1-distill-llama-70b | 0.52 | 0.75 | 0.99 |
| perplexity | sonar-pro | — | 3 | 15 |
| perplexity | sonar | — | 1 | 1 |
| perplexity | sonar-deep-research | 0.211 | 2 | 8 |
| perplexity | deep-research | 0.211 | 2 | 8 |
| perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 |
| perplexity | sonar-reasoning | 0.211 | 1 | 5 |
| bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 |
@@ -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 |

View File

@@ -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.

View File

@@ -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>'],

View File

@@ -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;
}
};

Some files were not shown because too many files have changed in this diff Show More