mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: add expand-task remote (#1384)
This commit is contained in:
@@ -18,7 +18,8 @@
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"chalk": "5.6.2",
|
||||
"boxen": "^8.0.1"
|
||||
"boxen": "^8.0.1",
|
||||
"ora": "^8.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
|
||||
197
packages/tm-bridge/src/expand-bridge.ts
Normal file
197
packages/tm-bridge/src/expand-bridge.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import ora from 'ora';
|
||||
import { createTmCore, type TmCore } from '@tm/core';
|
||||
|
||||
/**
|
||||
* Parameters for the expand bridge function
|
||||
*/
|
||||
export interface ExpandBridgeParams {
|
||||
/** Task ID (can be numeric "1" or alphanumeric "TAS-49") */
|
||||
taskId: string | number;
|
||||
/** Number of subtasks to generate (optional) */
|
||||
numSubtasks?: number;
|
||||
/** Whether to use research AI */
|
||||
useResearch?: boolean;
|
||||
/** Additional context for generation */
|
||||
additionalContext?: string;
|
||||
/** Force regeneration even if subtasks exist */
|
||||
force?: boolean;
|
||||
/** Project root directory */
|
||||
projectRoot: string;
|
||||
/** Optional tag for task organization */
|
||||
tag?: string;
|
||||
/** Whether called from MCP context (default: false) */
|
||||
isMCP?: boolean;
|
||||
/** Output format (default: 'text') */
|
||||
outputFormat?: 'text' | 'json';
|
||||
/** Logging function */
|
||||
report: (level: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned when API storage handles the expansion
|
||||
*/
|
||||
export interface RemoteExpandResult {
|
||||
success: boolean;
|
||||
taskId: string | number;
|
||||
message: string;
|
||||
telemetryData: null;
|
||||
tagInfo: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared bridge function for expand-task command.
|
||||
* Checks if using API storage and delegates to remote AI service if so.
|
||||
*
|
||||
* @param params - Bridge parameters
|
||||
* @returns Result object if API storage handled it, null if should fall through to file storage
|
||||
*/
|
||||
export async function tryExpandViaRemote(
|
||||
params: ExpandBridgeParams
|
||||
): Promise<RemoteExpandResult | null> {
|
||||
const {
|
||||
taskId,
|
||||
numSubtasks,
|
||||
useResearch = false,
|
||||
additionalContext,
|
||||
force = false,
|
||||
projectRoot,
|
||||
tag,
|
||||
isMCP = false,
|
||||
outputFormat = 'text',
|
||||
report
|
||||
} = params;
|
||||
|
||||
let tmCore: TmCore;
|
||||
|
||||
try {
|
||||
tmCore = await createTmCore({
|
||||
projectPath: projectRoot || process.cwd()
|
||||
});
|
||||
} catch (tmCoreError) {
|
||||
const errorMessage =
|
||||
tmCoreError instanceof Error ? tmCoreError.message : String(tmCoreError);
|
||||
report(
|
||||
'warn',
|
||||
`TmCore check failed, falling back to file-based expansion: ${errorMessage}`
|
||||
);
|
||||
// Return null to signal fall-through to file storage logic
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we're using API storage (use resolved storage type, not config)
|
||||
const storageType = tmCore.tasks.getStorageType();
|
||||
|
||||
if (storageType !== 'api') {
|
||||
// Not API storage - signal caller to fall through to file-based logic
|
||||
report(
|
||||
'debug',
|
||||
`Using file storage - processing expansion locally for task ${taskId}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// API STORAGE PATH: Delegate to remote AI service
|
||||
report('info', `Delegating expansion to Hamster for task ${taskId}`);
|
||||
|
||||
// Show CLI output if not MCP
|
||||
if (!isMCP && outputFormat === 'text') {
|
||||
const showDebug = process.env.TM_DEBUG === '1';
|
||||
const contextPreview =
|
||||
showDebug && additionalContext
|
||||
? `${additionalContext.substring(0, 60)}${additionalContext.length > 60 ? '...' : ''}`
|
||||
: additionalContext
|
||||
? '[provided]'
|
||||
: '[none]';
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.blue.bold(`Expanding Task via Hamster`) +
|
||||
'\n\n' +
|
||||
chalk.white(`Task ID: ${taskId}`) +
|
||||
'\n' +
|
||||
chalk.white(`Subtasks: ${numSubtasks || 'auto'}`) +
|
||||
'\n' +
|
||||
chalk.white(`Use Research: ${useResearch ? 'yes' : 'no'}`) +
|
||||
'\n' +
|
||||
chalk.white(`Force: ${force ? 'yes' : 'no'}`) +
|
||||
'\n' +
|
||||
chalk.white(`Context: ${contextPreview}`),
|
||||
{
|
||||
padding: 1,
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, bottom: 1 }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const spinner =
|
||||
!isMCP && outputFormat === 'text'
|
||||
? ora({ text: 'Expanding task on Hamster...', color: 'cyan' }).start()
|
||||
: null;
|
||||
|
||||
try {
|
||||
// Call the API storage method which handles the remote expansion
|
||||
const result = await tmCore.tasks.expand(String(taskId), tag, {
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
force
|
||||
});
|
||||
|
||||
if (spinner) {
|
||||
spinner.succeed('Task expansion queued successfully');
|
||||
}
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
// Build message conditionally based on result
|
||||
let messageLines = [
|
||||
chalk.green(`Successfully queued expansion for task ${taskId}`),
|
||||
'',
|
||||
chalk.white('The task expansion has been queued on Hamster'),
|
||||
chalk.white('Subtasks will be generated in the background.')
|
||||
];
|
||||
|
||||
// Add task link if available
|
||||
if (result?.taskLink) {
|
||||
messageLines.push('');
|
||||
messageLines.push(
|
||||
chalk.white('View task: ') + chalk.blue.underline(result.taskLink)
|
||||
);
|
||||
}
|
||||
|
||||
// Always add CLI alternative
|
||||
messageLines.push('');
|
||||
messageLines.push(
|
||||
chalk.dim(`Or run: ${chalk.yellow(`task-master show ${taskId}`)}`)
|
||||
);
|
||||
|
||||
console.log(
|
||||
boxen(messageLines.join('\n'), {
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
borderStyle: 'round'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Return success result - signals that we handled it
|
||||
return {
|
||||
success: true,
|
||||
taskId: taskId,
|
||||
message: result?.message || 'Task expansion queued via remote AI service',
|
||||
telemetryData: null,
|
||||
tagInfo: null
|
||||
};
|
||||
} catch (expandError) {
|
||||
if (spinner) {
|
||||
spinner.fail('Expansion failed');
|
||||
}
|
||||
|
||||
// tm-core already formatted the error properly, just re-throw
|
||||
throw expandError;
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,9 @@ export {
|
||||
type UpdateBridgeParams,
|
||||
type RemoteUpdateResult
|
||||
} from './update-bridge.js';
|
||||
|
||||
export {
|
||||
tryExpandViaRemote,
|
||||
type ExpandBridgeParams,
|
||||
type RemoteExpandResult
|
||||
} from './expand-bridge.js';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import ora from 'ora';
|
||||
import { createTmCore, type TmCore } from '@tm/core';
|
||||
|
||||
/**
|
||||
@@ -120,18 +121,10 @@ export async function tryUpdateViaRemote(
|
||||
);
|
||||
}
|
||||
|
||||
let loadingIndicator: NodeJS.Timeout | null = null;
|
||||
if (!isMCP && outputFormat === 'text') {
|
||||
// Simple loading indicator simulation (replace with actual startLoadingIndicator if available)
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
let frameIndex = 0;
|
||||
loadingIndicator = setInterval(() => {
|
||||
process.stdout.write(
|
||||
`\r${frames[frameIndex]} Updating task on Hamster...`
|
||||
);
|
||||
frameIndex = (frameIndex + 1) % frames.length;
|
||||
}, 80);
|
||||
}
|
||||
const spinner =
|
||||
!isMCP && outputFormat === 'text'
|
||||
? ora({ text: 'Updating task on Hamster...', color: 'cyan' }).start()
|
||||
: null;
|
||||
|
||||
try {
|
||||
// Call the API storage method which handles the remote update
|
||||
@@ -139,9 +132,8 @@ export async function tryUpdateViaRemote(
|
||||
mode
|
||||
});
|
||||
|
||||
if (loadingIndicator) {
|
||||
clearInterval(loadingIndicator);
|
||||
process.stdout.write('\r✓ Task updated successfully.\n');
|
||||
if (spinner) {
|
||||
spinner.succeed('Task updated successfully');
|
||||
}
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
@@ -172,9 +164,8 @@ export async function tryUpdateViaRemote(
|
||||
tagInfo: null
|
||||
};
|
||||
} catch (updateError) {
|
||||
if (loadingIndicator) {
|
||||
clearInterval(loadingIndicator);
|
||||
process.stdout.write('\r✗ Update failed.\n');
|
||||
if (spinner) {
|
||||
spinner.fail('Update failed');
|
||||
}
|
||||
|
||||
// tm-core already formatted the error properly, just re-throw
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
||||
import type { ExpandTaskResult } from '../../modules/integration/services/task-expansion.service.js';
|
||||
|
||||
/**
|
||||
* Options for loading tasks from storage
|
||||
@@ -92,6 +93,28 @@ export interface IStorage {
|
||||
options?: { useResearch?: boolean; mode?: 'append' | 'update' | 'rewrite' }
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Expand task into subtasks using AI-powered generation
|
||||
* @param taskId - ID of the task to expand
|
||||
* @param tag - Optional tag context for the task
|
||||
* @param options - Optional expansion options
|
||||
* @param options.numSubtasks - Number of subtasks to generate
|
||||
* @param options.useResearch - Whether to use research capabilities
|
||||
* @param options.additionalContext - Additional context for generation
|
||||
* @param options.force - Force regeneration even if subtasks exist
|
||||
* @returns ExpandTaskResult for API storage, void for file storage
|
||||
*/
|
||||
expandTaskWithPrompt(
|
||||
taskId: string,
|
||||
tag?: string,
|
||||
options?: {
|
||||
numSubtasks?: number;
|
||||
useResearch?: boolean;
|
||||
additionalContext?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): Promise<ExpandTaskResult | void>;
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID
|
||||
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
|
||||
@@ -260,6 +283,16 @@ export abstract class BaseStorage implements IStorage {
|
||||
tag?: string,
|
||||
options?: { useResearch?: boolean; mode?: 'append' | 'update' | 'rewrite' }
|
||||
): Promise<void>;
|
||||
abstract expandTaskWithPrompt(
|
||||
taskId: string,
|
||||
tag?: string,
|
||||
options?: {
|
||||
numSubtasks?: number;
|
||||
useResearch?: boolean;
|
||||
additionalContext?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): Promise<ExpandTaskResult | void>;
|
||||
abstract updateTaskStatus(
|
||||
taskId: string,
|
||||
newStatus: TaskStatus,
|
||||
|
||||
@@ -69,11 +69,13 @@ export class TaskMapper {
|
||||
createdAt: subtask.created_at,
|
||||
updatedAt: subtask.updated_at,
|
||||
assignee: subtask.assignee_id || undefined,
|
||||
complexity: subtask.complexity ?? undefined
|
||||
complexity: subtask.complexity ?? undefined,
|
||||
databaseId: subtask.id // Include the actual database UUID
|
||||
}));
|
||||
|
||||
return {
|
||||
id: dbTask.display_id || dbTask.id, // Use display_id if available
|
||||
databaseId: dbTask.id, // Include the actual database UUID
|
||||
title: dbTask.title,
|
||||
description: dbTask.description || '',
|
||||
status: this.mapStatus(dbTask.status),
|
||||
|
||||
@@ -73,6 +73,9 @@ export interface Task {
|
||||
tags?: string[];
|
||||
assignee?: string;
|
||||
|
||||
// Database UUID (for API calls that need the actual UUID instead of display_id)
|
||||
databaseId?: string;
|
||||
|
||||
// Complexity analysis (from complexity report)
|
||||
// Can be either enum ('simple' | 'moderate' | 'complex' | 'very-complex') or numeric score (1-10)
|
||||
complexity?: TaskComplexity | number;
|
||||
|
||||
@@ -423,7 +423,7 @@ export class ExportService {
|
||||
);
|
||||
} else {
|
||||
// Direct Supabase approach is no longer supported
|
||||
// The extractTasks method has been removed from SupabaseTaskRepository
|
||||
// The extractTasks method has been removed from SupabaseRepository
|
||||
// as we now exclusively use the API endpoint for exports
|
||||
throw new Error(
|
||||
'Export API endpoint not configured. Please set TM_PUBLIC_BASE_DOMAIN environment variable to enable task export.'
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @fileoverview Task Expansion Service
|
||||
* Core service for expanding tasks into subtasks using AI
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TaskMasterError
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
import type { TaskRepository } from '../../tasks/repositories/task-repository.interface.js';
|
||||
import { AuthManager } from '../../auth/managers/auth-manager.js';
|
||||
import { ApiClient } from '../../storage/utils/api-client.js';
|
||||
import { getLogger } from '../../../common/logger/factory.js';
|
||||
|
||||
/**
|
||||
* Response from the expand task API endpoint (202 Accepted)
|
||||
*/
|
||||
interface ExpandTaskResponse {
|
||||
message: string;
|
||||
taskId: string;
|
||||
queued: boolean;
|
||||
jobId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned to the caller with expansion details
|
||||
*/
|
||||
export interface ExpandTaskResult {
|
||||
/** Success message */
|
||||
message: string;
|
||||
/** Task ID (display_id like HAM-4) */
|
||||
taskId: string;
|
||||
/** Whether the job was queued successfully */
|
||||
queued: boolean;
|
||||
/** Background job ID for tracking */
|
||||
jobId: string;
|
||||
/** Direct link to view the task in the UI */
|
||||
taskLink: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for task expansion
|
||||
*/
|
||||
export interface ExpandTaskOptions {
|
||||
/** Number of subtasks to generate */
|
||||
numSubtasks?: number;
|
||||
/** Use research model for expansion */
|
||||
useResearch?: boolean;
|
||||
/** Additional context for AI generation */
|
||||
additionalContext?: string;
|
||||
/** Force expansion even if subtasks exist */
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth context with a guaranteed briefId
|
||||
*/
|
||||
type ContextWithBrief = NonNullable<
|
||||
ReturnType<typeof AuthManager.prototype.getContext>
|
||||
> & { briefId: string };
|
||||
|
||||
/**
|
||||
* TaskExpansionService handles AI-powered task expansion
|
||||
*/
|
||||
export class TaskExpansionService {
|
||||
private readonly repository: TaskRepository;
|
||||
private readonly projectId: string;
|
||||
private readonly apiClient: ApiClient;
|
||||
private readonly authManager: AuthManager;
|
||||
private readonly logger = getLogger('TaskExpansionService');
|
||||
|
||||
constructor(
|
||||
repository: TaskRepository,
|
||||
projectId: string,
|
||||
apiClient: ApiClient,
|
||||
authManager: AuthManager
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.projectId = projectId;
|
||||
this.apiClient = apiClient;
|
||||
this.authManager = authManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand task into subtasks with AI-powered generation
|
||||
* Sends task to backend for server-side AI processing
|
||||
* @returns Expansion result with job details and task link
|
||||
*/
|
||||
async expandTask(
|
||||
taskId: string,
|
||||
options?: ExpandTaskOptions
|
||||
): Promise<ExpandTaskResult> {
|
||||
try {
|
||||
// Get brief context from AuthManager
|
||||
const context = this.ensureBriefSelected('expandTask');
|
||||
|
||||
// Get the task being expanded to extract existing subtasks
|
||||
const task = await this.repository.getTask(this.projectId, taskId);
|
||||
|
||||
if (!task) {
|
||||
throw new TaskMasterError(
|
||||
`Task ${taskId} not found`,
|
||||
ERROR_CODES.VALIDATION_ERROR,
|
||||
{
|
||||
operation: 'expandTask',
|
||||
taskId,
|
||||
userMessage: `Task ${taskId} isn't available in the current project.`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Get brief information for enriched context
|
||||
const brief = await this.repository.getBrief(context.briefId);
|
||||
|
||||
// Build brief context payload with brief data if available
|
||||
const briefContext = {
|
||||
title: brief?.name || context.briefName || context.briefId,
|
||||
description: brief?.description || undefined,
|
||||
status: brief?.status || 'active'
|
||||
};
|
||||
|
||||
// Get all tasks for context (optional but helpful for AI)
|
||||
const allTasks = await this.repository.getTasks(this.projectId);
|
||||
|
||||
// Build the payload according to ExpandTaskContextSchema
|
||||
const payload = {
|
||||
briefContext,
|
||||
allTasks,
|
||||
existingSubtasks: task.subtasks || [],
|
||||
enrichedContext: options?.additionalContext
|
||||
};
|
||||
|
||||
// Build query params for options that aren't part of the context
|
||||
const queryParams = new URLSearchParams();
|
||||
if (options?.numSubtasks !== undefined) {
|
||||
queryParams.set('numSubtasks', options.numSubtasks.toString());
|
||||
}
|
||||
if (options?.useResearch !== undefined) {
|
||||
queryParams.set('useResearch', options.useResearch.toString());
|
||||
}
|
||||
if (options?.force !== undefined) {
|
||||
queryParams.set('force', options.force.toString());
|
||||
}
|
||||
|
||||
// Validate that task has a database UUID (required for API calls)
|
||||
if (!task.databaseId) {
|
||||
throw new TaskMasterError(
|
||||
`Task ${taskId} is missing a database ID. Task expansion requires tasks to be synced with the remote database.`,
|
||||
ERROR_CODES.VALIDATION_ERROR,
|
||||
{
|
||||
operation: 'expandTask',
|
||||
taskId,
|
||||
userMessage:
|
||||
'This task has not been synced with the remote database. Please ensure the task is saved remotely before attempting expansion.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Validate UUID format using Zod
|
||||
const uuidSchema = z.uuid();
|
||||
const validation = uuidSchema.safeParse(task.databaseId);
|
||||
if (!validation.success) {
|
||||
throw new TaskMasterError(
|
||||
`Task ${taskId} has an invalid database ID format: ${task.databaseId}`,
|
||||
ERROR_CODES.VALIDATION_ERROR,
|
||||
{
|
||||
operation: 'expandTask',
|
||||
taskId,
|
||||
databaseId: task.databaseId,
|
||||
userMessage:
|
||||
'The task database ID is not in valid UUID format. This may indicate data corruption.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Use validated databaseId (UUID) for API calls
|
||||
const taskUuid = task.databaseId;
|
||||
|
||||
const url = `/ai/api/v1/tasks/${taskUuid}/subtasks/generate${
|
||||
queryParams.toString() ? `?${queryParams.toString()}` : ''
|
||||
}`;
|
||||
|
||||
const result = await this.apiClient.post<ExpandTaskResponse>(
|
||||
url,
|
||||
payload
|
||||
);
|
||||
|
||||
// Get base URL for task link
|
||||
const baseUrl =
|
||||
process.env.TM_BASE_DOMAIN ||
|
||||
process.env.TM_PUBLIC_BASE_DOMAIN ||
|
||||
'http://localhost:8080';
|
||||
const taskLink = `${baseUrl}/home/hamster/briefs/${context.briefId}/task/${taskUuid}`;
|
||||
|
||||
// Log success with job details and task link
|
||||
this.logger.info(`✓ Task expansion queued for ${taskId}`);
|
||||
this.logger.info(` Job ID: ${result.jobId}`);
|
||||
this.logger.info(` ${result.message}`);
|
||||
this.logger.info(` View task: ${taskLink}`);
|
||||
|
||||
return {
|
||||
...result,
|
||||
taskLink
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's already a TaskMasterError, just add context and re-throw
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error.withContext({
|
||||
operation: 'expandTask',
|
||||
taskId,
|
||||
numSubtasks: options?.numSubtasks,
|
||||
useResearch: options?.useResearch
|
||||
});
|
||||
}
|
||||
|
||||
// For other errors, wrap them
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new TaskMasterError(
|
||||
errorMessage,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
{
|
||||
operation: 'expandTask',
|
||||
taskId,
|
||||
numSubtasks: options?.numSubtasks,
|
||||
useResearch: options?.useResearch
|
||||
},
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a brief is selected in the current context
|
||||
* @returns The current auth context with a valid briefId
|
||||
*/
|
||||
private ensureBriefSelected(operation: string): ContextWithBrief {
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
if (!context?.briefId) {
|
||||
throw new TaskMasterError(
|
||||
'No brief selected',
|
||||
ERROR_CODES.NO_BRIEF_SELECTED,
|
||||
{
|
||||
operation,
|
||||
userMessage:
|
||||
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return context as ContextWithBrief;
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,15 @@ import {
|
||||
TaskMasterError
|
||||
} from '../../../common/errors/task-master-error.js';
|
||||
import { TaskRepository } from '../../tasks/repositories/task-repository.interface.js';
|
||||
import { SupabaseTaskRepository } from '../../tasks/repositories/supabase/index.js';
|
||||
import { SupabaseRepository } from '../../tasks/repositories/supabase/index.js';
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { AuthManager } from '../../auth/managers/auth-manager.js';
|
||||
import { ApiClient } from '../utils/api-client.js';
|
||||
import { getLogger } from '../../../common/logger/factory.js';
|
||||
import {
|
||||
ExpandTaskResult,
|
||||
TaskExpansionService
|
||||
} from '../../integration/services/task-expansion.service.js';
|
||||
|
||||
/**
|
||||
* API storage configuration
|
||||
@@ -77,6 +81,7 @@ export class ApiStorage implements IStorage {
|
||||
private initialized = false;
|
||||
private tagsCache: Map<string, TaskTag> = new Map();
|
||||
private apiClient?: ApiClient;
|
||||
private expansionService?: TaskExpansionService;
|
||||
private readonly logger = getLogger('ApiStorage');
|
||||
|
||||
constructor(config: ApiStorageConfig) {
|
||||
@@ -86,9 +91,9 @@ export class ApiStorage implements IStorage {
|
||||
if (config.repository) {
|
||||
this.repository = config.repository;
|
||||
} else if (config.supabaseClient) {
|
||||
// TODO: SupabaseTaskRepository doesn't implement all TaskRepository methods yet
|
||||
// TODO: SupabaseRepository doesn't implement all TaskRepository methods yet
|
||||
// Cast for now until full implementation is complete
|
||||
this.repository = new SupabaseTaskRepository(
|
||||
this.repository = new SupabaseRepository(
|
||||
config.supabaseClient
|
||||
) as unknown as TaskRepository;
|
||||
} else {
|
||||
@@ -601,6 +606,26 @@ export class ApiStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand task into subtasks with AI-powered generation
|
||||
* Sends task to backend for server-side AI processing
|
||||
*/
|
||||
async expandTaskWithPrompt(
|
||||
taskId: string,
|
||||
_tag?: string,
|
||||
options?: {
|
||||
numSubtasks?: number;
|
||||
useResearch?: boolean;
|
||||
additionalContext?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): Promise<ExpandTaskResult> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const expansionService = this.getExpansionService();
|
||||
return await expansionService.expandTask(taskId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID - for API storage
|
||||
*/
|
||||
@@ -925,6 +950,25 @@ export class ApiStorage implements IStorage {
|
||||
return this.apiClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create TaskExpansionService instance
|
||||
*/
|
||||
private getExpansionService(): TaskExpansionService {
|
||||
if (!this.expansionService) {
|
||||
const apiClient = this.getApiClient();
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
this.expansionService = new TaskExpansionService(
|
||||
this.repository,
|
||||
this.projectId,
|
||||
apiClient,
|
||||
authManager
|
||||
);
|
||||
}
|
||||
|
||||
return this.expansionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry an operation with exponential backoff
|
||||
*/
|
||||
|
||||
@@ -396,6 +396,26 @@ export class FileStorage implements IStorage {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand task into subtasks with AI-powered generation
|
||||
* For file storage, this should NOT be called - client must handle AI processing first
|
||||
*/
|
||||
async expandTaskWithPrompt(
|
||||
_taskId: string,
|
||||
_tag?: string,
|
||||
_options?: {
|
||||
numSubtasks?: number;
|
||||
useResearch?: boolean;
|
||||
additionalContext?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
throw new Error(
|
||||
'File storage does not support expandTaskWithPrompt. ' +
|
||||
'Client-side AI logic must process the expansion before calling updateTask().'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Supabase repository implementations
|
||||
*/
|
||||
export { SupabaseTaskRepository } from './supabase-task-repository.js';
|
||||
export { SupabaseRepository } from './supabase-repository.js';
|
||||
export { DependencyFetcher } from './dependency-fetcher.js';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TaskDatabaseUpdate
|
||||
} from '../../../../common/types/repository-types.js';
|
||||
import { LoadTasksOptions } from '../../../../common/interfaces/storage.interface.js';
|
||||
import { Brief } from '../task-repository.interface.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Zod schema for task status validation
|
||||
@@ -34,7 +35,7 @@ const TaskUpdateSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
export class SupabaseTaskRepository {
|
||||
export class SupabaseRepository {
|
||||
private dependencyFetcher: DependencyFetcher;
|
||||
private authManager: AuthManager;
|
||||
|
||||
@@ -152,6 +153,50 @@ export class SupabaseTaskRepository {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brief information by ID
|
||||
* Note: This doesn't use getBriefIdOrThrow() because we may need to fetch
|
||||
* briefs other than the current context brief
|
||||
*/
|
||||
async getBrief(briefId: string): Promise<Brief | null> {
|
||||
const { data, error } = await this.supabase
|
||||
.from('brief')
|
||||
.select(`
|
||||
*,
|
||||
document:document_id (
|
||||
id,
|
||||
title,
|
||||
description
|
||||
)
|
||||
`)
|
||||
.eq('id', briefId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
return null; // Not found
|
||||
}
|
||||
throw new Error(`Failed to fetch brief: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract document data if available
|
||||
const document = data.document as any;
|
||||
|
||||
// Map database fields to Brief interface
|
||||
return {
|
||||
id: data.id,
|
||||
accountId: data.account_id,
|
||||
createdAt: data.created_at,
|
||||
name: document?.title || undefined,
|
||||
description: document?.description || undefined,
|
||||
status: data.status || undefined
|
||||
};
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
@@ -1,6 +1,18 @@
|
||||
import { Task, TaskTag } from '../../../common/types/index.js';
|
||||
import { LoadTasksOptions } from '../../../common/interfaces/storage.interface.js';
|
||||
|
||||
/**
|
||||
* Brief information
|
||||
*/
|
||||
export interface Brief {
|
||||
id: string;
|
||||
accountId: string;
|
||||
createdAt: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface TaskRepository {
|
||||
// Task operations
|
||||
getTasks(projectId: string, options?: LoadTasksOptions): Promise<Task[]>;
|
||||
@@ -13,6 +25,9 @@ export interface TaskRepository {
|
||||
): Promise<Task>;
|
||||
deleteTask(projectId: string, taskId: string): Promise<void>;
|
||||
|
||||
// Brief operations
|
||||
getBrief(briefId: string): Promise<Brief | null>;
|
||||
|
||||
// Tag operations
|
||||
getTags(projectId: string): Promise<TaskTag[]>;
|
||||
getTag(projectId: string, tagName: string): Promise<TaskTag | null>;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { StorageFactory } from '../../storage/services/storage-factory.js';
|
||||
import { TaskEntity } from '../entities/task.entity.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js';
|
||||
import { getLogger } from '../../../common/logger/factory.js';
|
||||
import type { ExpandTaskResult } from '../../integration/services/task-expansion.service.js';
|
||||
|
||||
/**
|
||||
* Result returned by getTaskList
|
||||
@@ -632,6 +633,76 @@ export class TaskService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a task into subtasks using AI-powered generation
|
||||
* @param taskId - Task ID to expand (supports numeric and alphanumeric IDs)
|
||||
* @param tag - Optional tag context
|
||||
* @param options - Optional expansion options
|
||||
* @param options.numSubtasks - Number of subtasks to generate
|
||||
* @param options.useResearch - Use research AI for generation
|
||||
* @param options.additionalContext - Additional context for generation
|
||||
* @param options.force - Force regeneration even if subtasks exist
|
||||
* @returns ExpandTaskResult when using API storage, void for file storage
|
||||
*/
|
||||
async expandTaskWithPrompt(
|
||||
taskId: string | number,
|
||||
tag?: string,
|
||||
options?: {
|
||||
numSubtasks?: number;
|
||||
useResearch?: boolean;
|
||||
additionalContext?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): Promise<ExpandTaskResult | void> {
|
||||
// Ensure we have storage
|
||||
if (!this.storage) {
|
||||
throw new TaskMasterError(
|
||||
'Storage not initialized',
|
||||
ERROR_CODES.STORAGE_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-initialize if needed
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// Use provided tag or get active tag
|
||||
const activeTag = tag || this.getActiveTag();
|
||||
const taskIdStr = String(taskId);
|
||||
|
||||
try {
|
||||
// AI-powered expansion - send to storage layer
|
||||
// API storage: sends request to backend for server-side AI processing
|
||||
// File storage: must use client-side AI logic before calling this
|
||||
return await this.storage.expandTaskWithPrompt(
|
||||
taskIdStr,
|
||||
activeTag,
|
||||
options
|
||||
);
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
||||
if (
|
||||
error instanceof TaskMasterError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new TaskMasterError(
|
||||
`Failed to expand task ${taskId}`,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
{
|
||||
operation: 'expandTaskWithPrompt',
|
||||
resource: 'task',
|
||||
taskId: taskIdStr,
|
||||
tag: activeTag,
|
||||
numSubtasks: options?.numSubtasks
|
||||
},
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status - delegates to storage layer which handles storage-specific logic
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
PreflightResult
|
||||
} from './services/preflight-checker.service.js';
|
||||
import type { TaskValidationResult } from './services/task-loader.service.js';
|
||||
import type { ExpandTaskResult } from '../integration/services/task-expansion.service.js';
|
||||
|
||||
/**
|
||||
* Tasks Domain - Unified API for all task operations
|
||||
@@ -151,6 +152,23 @@ export class TasksDomain {
|
||||
return this.taskService.updateTaskWithPrompt(taskId, prompt, tag, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand task into subtasks using AI
|
||||
* @returns ExpandTaskResult when using API storage, void for file storage
|
||||
*/
|
||||
async expand(
|
||||
taskId: string | number,
|
||||
tag?: string,
|
||||
options?: {
|
||||
numSubtasks?: number;
|
||||
useResearch?: boolean;
|
||||
additionalContext?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
): Promise<ExpandTaskResult | void> {
|
||||
return this.taskService.expandTaskWithPrompt(taskId, tag, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status
|
||||
*/
|
||||
|
||||
35
scripts/modules/bridge-utils.js
Normal file
35
scripts/modules/bridge-utils.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isSilentMode, log as consoleLog } from './utils.js';
|
||||
import { getDebugFlag } from './config-manager.js';
|
||||
|
||||
/**
|
||||
* Create a unified logger and report function for bridge operations
|
||||
* Handles both MCP and CLI contexts consistently
|
||||
*
|
||||
* @param {Object} mcpLog - Optional MCP logger object
|
||||
* @param {Object} [session] - Optional session object for debug flag checking
|
||||
* @returns {Object} Object containing logger, report function, and isMCP flag
|
||||
*/
|
||||
export function createBridgeLogger(mcpLog, session) {
|
||||
const isMCP = !!mcpLog;
|
||||
|
||||
// Create logger that works in both contexts
|
||||
const logger = mcpLog || {
|
||||
info: (msg) => !isSilentMode() && consoleLog('info', msg),
|
||||
warn: (msg) => !isSilentMode() && consoleLog('warn', msg),
|
||||
error: (msg) => !isSilentMode() && consoleLog('error', msg),
|
||||
debug: (msg) =>
|
||||
!isSilentMode() && getDebugFlag(session) && consoleLog('debug', msg)
|
||||
};
|
||||
|
||||
// Create report function compatible with bridge
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logger[level] === 'function') logger[level](...args);
|
||||
else logger.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
consoleLog(level, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
return { logger, report, isMCP };
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
getTagAwareFilePath,
|
||||
isSilentMode,
|
||||
log,
|
||||
readJSON,
|
||||
writeJSON
|
||||
} from '../utils.js';
|
||||
import { readJSON, writeJSON } from '../utils.js';
|
||||
|
||||
import {
|
||||
displayAiUsageSummary,
|
||||
@@ -20,13 +13,15 @@ import { generateObjectService } from '../ai-services-unified.js';
|
||||
|
||||
import {
|
||||
getDefaultSubtasks,
|
||||
getDebugFlag,
|
||||
hasCodebaseAnalysis
|
||||
hasCodebaseAnalysis,
|
||||
getDebugFlag
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import { findProjectRoot, flattenTasksWithSubtasks } from '../utils.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { tryExpandViaRemote } from '@tm/bridge';
|
||||
import { createBridgeLogger } from '../bridge-utils.js';
|
||||
|
||||
/**
|
||||
* Expand a task into subtasks using the unified AI service (generateObjectService).
|
||||
@@ -68,20 +63,35 @@ async function expandTask(
|
||||
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
|
||||
const projectRoot = contextProjectRoot || findProjectRoot(tasksPath);
|
||||
|
||||
// Use mcpLog if available, otherwise use the default console log wrapper
|
||||
const logger = mcpLog || {
|
||||
info: (msg) => !isSilentMode() && log('info', msg),
|
||||
warn: (msg) => !isSilentMode() && log('warn', msg),
|
||||
error: (msg) => !isSilentMode() && log('error', msg),
|
||||
debug: (msg) =>
|
||||
!isSilentMode() && getDebugFlag(session) && log('debug', msg) // Use getDebugFlag
|
||||
};
|
||||
// Create unified logger and report function
|
||||
const { logger, report, isMCP } = createBridgeLogger(mcpLog, session);
|
||||
|
||||
if (mcpLog) {
|
||||
if (isMCP) {
|
||||
logger.info(`expandTask called with context: session=${!!session}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// --- BRIDGE: Try remote expansion first (API storage) ---
|
||||
const remoteResult = await tryExpandViaRemote({
|
||||
taskId,
|
||||
numSubtasks,
|
||||
useResearch,
|
||||
additionalContext,
|
||||
force,
|
||||
projectRoot,
|
||||
tag,
|
||||
isMCP,
|
||||
outputFormat,
|
||||
report
|
||||
});
|
||||
|
||||
// If remote handled it, return the result
|
||||
if (remoteResult) {
|
||||
return remoteResult;
|
||||
}
|
||||
// Otherwise fall through to file-based logic below
|
||||
// --- End BRIDGE ---
|
||||
|
||||
// --- Task Loading/Filtering (Unchanged) ---
|
||||
logger.info(`Reading tasks from ${tasksPath}`);
|
||||
const data = readJSON(tasksPath, projectRoot, tag);
|
||||
@@ -275,7 +285,7 @@ async function expandTask(
|
||||
}
|
||||
|
||||
const { systemPrompt, userPrompt: promptContent } =
|
||||
await promptManager.loadPrompt('expand-task', promptParams, variantKey);
|
||||
promptManager.loadPrompt('expand-task', promptParams, variantKey);
|
||||
|
||||
// Debug logging to identify the issue
|
||||
logger.debug(`Selected variant: ${variantKey}`);
|
||||
|
||||
@@ -4,11 +4,9 @@ import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
|
||||
import {
|
||||
log as consoleLog,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
truncate,
|
||||
isSilentMode,
|
||||
flattenTasksWithSubtasks,
|
||||
findProjectRoot
|
||||
} from '../utils.js';
|
||||
@@ -26,14 +24,15 @@ import {
|
||||
} from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
isApiKeySet,
|
||||
hasCodebaseAnalysis
|
||||
hasCodebaseAnalysis,
|
||||
getDebugFlag
|
||||
} from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { tryUpdateViaRemote } from '@tm/bridge';
|
||||
import { createBridgeLogger } from '../bridge-utils.js';
|
||||
|
||||
/**
|
||||
* Update a task by ID with new information using the unified AI service.
|
||||
@@ -60,18 +59,7 @@ async function updateTaskById(
|
||||
appendMode = false
|
||||
) {
|
||||
const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
|
||||
const logFn = mcpLog || consoleLog;
|
||||
const isMCP = !!mcpLog;
|
||||
|
||||
// Use report helper for logging
|
||||
const report = (level, ...args) => {
|
||||
if (isMCP) {
|
||||
if (typeof logFn[level] === 'function') logFn[level](...args);
|
||||
else logFn.info(...args);
|
||||
} else if (!isSilentMode()) {
|
||||
logFn(level, ...args);
|
||||
}
|
||||
};
|
||||
const { report, isMCP } = createBridgeLogger(mcpLog, session);
|
||||
|
||||
try {
|
||||
report('info', `Updating single task ${taskId} with prompt: "${prompt}"`);
|
||||
@@ -294,7 +282,7 @@ async function updateTaskById(
|
||||
let systemPrompt;
|
||||
let userPrompt;
|
||||
try {
|
||||
const promptResult = await promptManager.loadPrompt(
|
||||
const promptResult = promptManager.loadPrompt(
|
||||
'update-task',
|
||||
promptParams,
|
||||
variantKey
|
||||
@@ -571,10 +559,8 @@ async function updateTaskById(
|
||||
// ... helpful hints ...
|
||||
if (getDebugFlag(session)) console.error(error);
|
||||
process.exit(1);
|
||||
} else {
|
||||
throw error; // Re-throw for MCP
|
||||
}
|
||||
return null; // Indicate failure in CLI case if process doesn't exit
|
||||
throw error; // Re-throw for MCP
|
||||
// --- End General Error Handling ---
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ jest.unstable_mockModule(
|
||||
'../../../../../scripts/modules/prompt-manager.js',
|
||||
() => ({
|
||||
getPromptManager: jest.fn().mockReturnValue({
|
||||
loadPrompt: jest.fn().mockResolvedValue({
|
||||
loadPrompt: jest.fn().mockReturnValue({
|
||||
systemPrompt: 'Mocked system prompt',
|
||||
userPrompt: 'Mocked user prompt'
|
||||
})
|
||||
|
||||
@@ -162,7 +162,7 @@ jest.unstable_mockModule(
|
||||
'../../../../../scripts/modules/prompt-manager.js',
|
||||
() => ({
|
||||
getPromptManager: jest.fn().mockReturnValue({
|
||||
loadPrompt: jest.fn().mockResolvedValue({
|
||||
loadPrompt: jest.fn().mockReturnValue({
|
||||
systemPrompt: 'Mocked system prompt',
|
||||
userPrompt: 'Mocked user prompt'
|
||||
})
|
||||
@@ -182,7 +182,12 @@ jest.unstable_mockModule('chalk', () => ({
|
||||
),
|
||||
green: jest.fn((text) => text),
|
||||
yellow: jest.fn((text) => text),
|
||||
bold: jest.fn((text) => text)
|
||||
red: jest.fn((text) => text),
|
||||
blue: jest.fn((text) => text),
|
||||
magenta: jest.fn((text) => text),
|
||||
gray: jest.fn((text) => text),
|
||||
bold: jest.fn((text) => text),
|
||||
dim: jest.fn((text) => text)
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -702,7 +707,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt: 'Generate exactly 5 subtasks for complexity report',
|
||||
userPrompt:
|
||||
'Please break this task into 5 parts\n\nUser provided context'
|
||||
@@ -1016,7 +1021,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt: 'Mocked system prompt',
|
||||
userPrompt: 'Mocked user prompt with context'
|
||||
});
|
||||
@@ -1145,7 +1150,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt:
|
||||
'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into an appropriate number of specific subtasks that can be implemented one by one.',
|
||||
userPrompt:
|
||||
@@ -1173,7 +1178,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt:
|
||||
'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 5 specific subtasks that can be implemented one by one.',
|
||||
userPrompt: 'Break down this task into exactly 5 specific subtasks'
|
||||
@@ -1201,7 +1206,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt:
|
||||
'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 4 specific subtasks that can be implemented one by one.',
|
||||
userPrompt: 'Break down this task into exactly 4 specific subtasks'
|
||||
@@ -1227,7 +1232,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt:
|
||||
'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 6 specific subtasks that can be implemented one by one.',
|
||||
userPrompt: 'Break down this task into exactly 6 specific subtasks'
|
||||
@@ -1253,7 +1258,7 @@ describe('expandTask', () => {
|
||||
const { getPromptManager } = await import(
|
||||
'../../../../../scripts/modules/prompt-manager.js'
|
||||
);
|
||||
const mockLoadPrompt = jest.fn().mockResolvedValue({
|
||||
const mockLoadPrompt = jest.fn().mockReturnValue({
|
||||
systemPrompt:
|
||||
'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 7 specific subtasks that can be implemented one by one.',
|
||||
userPrompt: 'Break down this task into exactly 7 specific subtasks'
|
||||
|
||||
Reference in New Issue
Block a user