177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
/**
|
|
* add command implementation
|
|
*
|
|
* Starts an AI conversation to refine task requirements,
|
|
* then creates a task file in .takt/tasks/ with YAML format.
|
|
*/
|
|
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { stringify as stringifyYaml } from 'yaml';
|
|
import { promptInput, confirm, selectOption } from '../prompt/index.js';
|
|
import { success, info } from '../utils/ui.js';
|
|
import { summarizeTaskName } from '../task/summarize.js';
|
|
import { loadGlobalConfig } from '../config/globalConfig.js';
|
|
import { getProvider, type ProviderType } from '../providers/index.js';
|
|
import { createLogger } from '../utils/debug.js';
|
|
import { listWorkflows } from '../config/workflowLoader.js';
|
|
import { getCurrentWorkflow } from '../config/paths.js';
|
|
import { interactiveMode } from './interactive.js';
|
|
import { isIssueReference, resolveIssueTask } from '../github/issue.js';
|
|
import type { TaskFileData } from '../task/schema.js';
|
|
|
|
const log = createLogger('add-task');
|
|
|
|
const SUMMARIZE_SYSTEM_PROMPT = `会話履歴からタスクの要件をまとめてください。
|
|
タスク実行エージェントへの指示として使われます。
|
|
具体的・簡潔に、必要な情報をすべて含めてください。
|
|
マークダウン形式で出力してください。`;
|
|
|
|
/**
|
|
* Summarize conversation history into a task description using AI.
|
|
*/
|
|
export async function summarizeConversation(cwd: string, conversationText: string): Promise<string> {
|
|
const globalConfig = loadGlobalConfig();
|
|
const providerType = (globalConfig.provider as ProviderType) ?? 'claude';
|
|
const provider = getProvider(providerType);
|
|
|
|
info('Summarizing task from conversation...');
|
|
|
|
const response = await provider.call('task-summarizer', conversationText, {
|
|
cwd,
|
|
maxTurns: 1,
|
|
allowedTools: [],
|
|
systemPrompt: SUMMARIZE_SYSTEM_PROMPT,
|
|
});
|
|
|
|
return response.content;
|
|
}
|
|
|
|
/**
|
|
* Generate a unique task filename with AI-summarized slug
|
|
*/
|
|
async function generateFilename(tasksDir: string, taskContent: string, cwd: string): Promise<string> {
|
|
info('Generating task filename...');
|
|
const slug = await summarizeTaskName(taskContent, { cwd });
|
|
const base = slug || 'task';
|
|
let filename = `${base}.yaml`;
|
|
let counter = 1;
|
|
|
|
while (fs.existsSync(path.join(tasksDir, filename))) {
|
|
filename = `${base}-${counter}.yaml`;
|
|
counter++;
|
|
}
|
|
|
|
return filename;
|
|
}
|
|
|
|
/**
|
|
* add command handler
|
|
*
|
|
* Flow:
|
|
* 1. AI対話モードでタスクを詰める
|
|
* 2. 会話履歴からAIがタスク要約を生成
|
|
* 3. 要約からファイル名をAIで生成
|
|
* 4. ワークツリー/ブランチ/ワークフロー設定
|
|
* 5. YAMLファイル作成
|
|
*/
|
|
export async function addTask(cwd: string, task?: string): Promise<void> {
|
|
const tasksDir = path.join(cwd, '.takt', 'tasks');
|
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
|
|
let taskContent: string;
|
|
|
|
if (task && isIssueReference(task)) {
|
|
// Issue reference: fetch issue and use directly as task content
|
|
info('Fetching GitHub Issue...');
|
|
try {
|
|
taskContent = resolveIssueTask(task);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
log.error('Failed to fetch GitHub Issue', { task, error: msg });
|
|
info(`Failed to fetch issue ${task}: ${msg}`);
|
|
return;
|
|
}
|
|
} else {
|
|
// Interactive mode: AI conversation to refine task
|
|
const result = await interactiveMode(cwd);
|
|
if (!result.confirmed) {
|
|
info('Cancelled.');
|
|
return;
|
|
}
|
|
|
|
// 会話履歴からタスク要約を生成
|
|
taskContent = await summarizeConversation(cwd, result.task);
|
|
}
|
|
|
|
// 3. 要約からファイル名生成
|
|
const firstLine = taskContent.split('\n')[0] || taskContent;
|
|
const filename = await generateFilename(tasksDir, firstLine, cwd);
|
|
|
|
// 4. ワークツリー/ブランチ/ワークフロー設定
|
|
let worktree: boolean | string | undefined;
|
|
let branch: string | undefined;
|
|
let workflow: string | undefined;
|
|
|
|
const useWorktree = await confirm('Create worktree?', false);
|
|
if (useWorktree) {
|
|
const customPath = await promptInput('Worktree path (Enter for auto)');
|
|
worktree = customPath || true;
|
|
|
|
const customBranch = await promptInput('Branch name (Enter for auto)');
|
|
if (customBranch) {
|
|
branch = customBranch;
|
|
}
|
|
}
|
|
|
|
const availableWorkflows = listWorkflows();
|
|
if (availableWorkflows.length > 0) {
|
|
const currentWorkflow = getCurrentWorkflow(cwd);
|
|
const defaultWorkflow = availableWorkflows.includes(currentWorkflow)
|
|
? currentWorkflow
|
|
: availableWorkflows[0]!;
|
|
const options = availableWorkflows.map((name) => ({
|
|
label: name === currentWorkflow ? `${name} (current)` : name,
|
|
value: name,
|
|
}));
|
|
const selected = await selectOption('Select workflow:', options);
|
|
if (selected === null) {
|
|
info('Cancelled.');
|
|
return;
|
|
}
|
|
if (selected !== defaultWorkflow) {
|
|
workflow = selected;
|
|
}
|
|
}
|
|
|
|
// 5. YAMLファイル作成
|
|
const taskData: TaskFileData = { task: taskContent };
|
|
if (worktree !== undefined) {
|
|
taskData.worktree = worktree;
|
|
}
|
|
if (branch) {
|
|
taskData.branch = branch;
|
|
}
|
|
if (workflow) {
|
|
taskData.workflow = workflow;
|
|
}
|
|
|
|
const filePath = path.join(tasksDir, filename);
|
|
const yamlContent = stringifyYaml(taskData);
|
|
fs.writeFileSync(filePath, yamlContent, 'utf-8');
|
|
|
|
log.info('Task created', { filePath, taskData });
|
|
|
|
success(`Task created: ${filename}`);
|
|
info(` Path: ${filePath}`);
|
|
if (worktree) {
|
|
info(` Worktree: ${typeof worktree === 'string' ? worktree : 'auto'}`);
|
|
}
|
|
if (branch) {
|
|
info(` Branch: ${branch}`);
|
|
}
|
|
if (workflow) {
|
|
info(` Workflow: ${workflow}`);
|
|
}
|
|
}
|