worktreeは一時Omit

This commit is contained in:
nrslib 2026-01-26 15:40:10 +09:00
parent bed5662097
commit 65a9553bb9
7 changed files with 2 additions and 242 deletions

View File

@ -59,23 +59,8 @@ Determine the implementation direction:
``` ```
[PLANNER:DONE] [PLANNER:DONE]
worktree:
baseBranch: {base branch name}
branchName: {new branch name}
``` ```
**baseBranch criteria:**
- New feature: `main` or `master`
- Existing feature modification: related feature branch (use `main` if unknown)
- Bug fix: relevant branch (use `main` if unknown)
**branchName naming convention:**
- Feature addition: `add-{feature-name}` (e.g., `add-user-authentication`)
- Fix: `fix-{issue}` (e.g., `fix-login-error`)
- Refactor: `refactor-{target}` (e.g., `refactor-api-client`)
- Use lowercase English with hyphens
### BLOCKED Output Structure ### BLOCKED Output Structure
``` ```
@ -86,8 +71,6 @@ Clarifications needed:
- {Question 2} - {Question 2}
``` ```
**Note:** Do not output worktree settings when BLOCKED.
## Important ## Important
**Keep analysis simple.** Overly detailed plans are unnecessary. Provide enough direction for Coder to proceed with implementation. **Keep analysis simple.** Overly detailed plans are unnecessary. Provide enough direction for Coder to proceed with implementation.

View File

@ -59,23 +59,8 @@
``` ```
[PLANNER:DONE] [PLANNER:DONE]
worktree:
baseBranch: {元ブランチ名}
branchName: {新ブランチ名}
``` ```
**baseBranch判断基準:**
- 新機能開発: `main` または `master`
- 既存機能の修正: 関連するfeatureブランチ不明な場合は `main`
- バグ修正: 該当するブランチ(不明な場合は `main`
**branchName命名規則:**
- 機能追加: `add-{feature-name}` (例: `add-user-authentication`)
- 修正: `fix-{issue}` (例: `fix-login-error`)
- リファクタ: `refactor-{target}` (例: `refactor-api-client`)
- 英語・小文字・ハイフン区切りで記述
### BLOCKED時の出力構造 ### BLOCKED時の出力構造
``` ```
@ -86,8 +71,6 @@ worktree:
- {質問2} - {質問2}
``` ```
**注意:** BLOCKEDの場合、worktree設定は出力しない。
## 重要 ## 重要
**シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。 **シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。

View File

@ -22,7 +22,6 @@ import {
} from '../utils/session.js'; } from '../utils/session.js';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../utils/debug.js';
import { notifySuccess, notifyError } from '../utils/notification.js'; import { notifySuccess, notifyError } from '../utils/notification.js';
import { createWorktree, type WorktreeInfo, type WorktreeConfig } from '../utils/worktree.js';
const log = createLogger('workflow'); const log = createLogger('workflow');
@ -30,8 +29,6 @@ const log = createLogger('workflow');
export interface WorkflowExecutionResult { export interface WorkflowExecutionResult {
success: boolean; success: boolean;
reason?: string; reason?: string;
/** Worktree information if worktree mode was used */
worktree?: WorktreeInfo;
} }
/** Options for workflow execution */ /** Options for workflow execution */
@ -44,10 +41,6 @@ export interface WorkflowExecutionOptions {
/** /**
* Execute a workflow and handle all events * Execute a workflow and handle all events
*
* Worktree creation is determined by Planner:
* - If Planner outputs [PLANNER:DONE] with worktree config, a worktree is created
* - If Planner outputs [PLANNER:BLOCKED], no worktree is created
*/ */
export async function executeWorkflow( export async function executeWorkflow(
workflowConfig: WorkflowConfig, workflowConfig: WorkflowConfig,
@ -60,9 +53,6 @@ export async function executeWorkflow(
headerPrefix = 'Running Workflow:', headerPrefix = 'Running Workflow:',
} = options; } = options;
// Worktree info will be set when Planner emits worktree config
let worktreeInfo: WorktreeInfo | undefined;
// Clear previous sessions if not resuming // Clear previous sessions if not resuming
if (!resumeSession) { if (!resumeSession) {
log.debug('Starting fresh session (clearing previous agent sessions)'); log.debug('Starting fresh session (clearing previous agent sessions)');
@ -92,7 +82,6 @@ export async function executeWorkflow(
const savedSessions = loadAgentSessions(cwd); const savedSessions = loadAgentSessions(cwd);
// Session update handler - persist session IDs when they change // Session update handler - persist session IDs when they change
// Always use original cwd for .takt data (案C: worktreeはコード作業専用)
const sessionUpdateHandler = (agentName: string, agentSessionId: string): void => { const sessionUpdateHandler = (agentName: string, agentSessionId: string): void => {
updateAgentSession(cwd, agentName, agentSessionId); updateAgentSession(cwd, agentName, agentSessionId);
}; };
@ -126,25 +115,6 @@ export async function executeWorkflow(
addToSessionLog(sessionLog, step.name, response); addToSessionLog(sessionLog, step.name, response);
}); });
// Handle worktree config from Planner
engine.on('planner:worktree_config', (config: WorktreeConfig) => {
log.info('Planner provided worktree config', config);
try {
info(`Creating worktree for branch: ${config.branchName}`);
worktreeInfo = createWorktree(cwd, config.branchName, config.baseBranch);
success(`Worktree created: ${worktreeInfo.path}`);
info(`Base branch: ${worktreeInfo.baseBranch}`);
info(`Working in worktree: ${worktreeInfo.path}`);
// Update engine's cwd to worktree path for remaining steps
engine.updateCwd(worktreeInfo.path);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
error(`Failed to create worktree: ${errorMessage}`);
// Continue without worktree - don't abort the workflow
}
});
engine.on('workflow:complete', (state) => { engine.on('workflow:complete', (state) => {
log.info('Workflow completed successfully', { iterations: state.iteration }); log.info('Workflow completed successfully', { iterations: state.iteration });
finalizeSessionLog(sessionLog, 'completed'); finalizeSessionLog(sessionLog, 'completed');
@ -152,10 +122,6 @@ export async function executeWorkflow(
const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd); const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd);
success(`Workflow completed (${state.iteration} iterations)`); success(`Workflow completed (${state.iteration} iterations)`);
info(`Session log: ${logPath}`); info(`Session log: ${logPath}`);
if (worktreeInfo) {
info(`Worktree preserved at: ${worktreeInfo.path}`);
info(`Branch: ${worktreeInfo.branch}`);
}
notifySuccess('TAKT', `ワークフロー完了 (${state.iteration} iterations)`); notifySuccess('TAKT', `ワークフロー完了 (${state.iteration} iterations)`);
}); });
@ -171,10 +137,6 @@ export async function executeWorkflow(
const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd); const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd);
error(`Workflow aborted after ${state.iteration} iterations: ${reason}`); error(`Workflow aborted after ${state.iteration} iterations: ${reason}`);
info(`Session log: ${logPath}`); info(`Session log: ${logPath}`);
if (worktreeInfo) {
info(`Worktree preserved at: ${worktreeInfo.path}`);
info(`Branch: ${worktreeInfo.branch}`);
}
notifyError('TAKT', `中断: ${reason}`); notifyError('TAKT', `中断: ${reason}`);
}); });
@ -183,6 +145,5 @@ export async function executeWorkflow(
return { return {
success: finalState.status === 'completed', success: finalState.status === 'completed',
reason: abortReason, reason: abortReason,
worktree: worktreeInfo,
}; };
} }

View File

@ -5,4 +5,3 @@
export * from './ui.js'; export * from './ui.js';
export * from './session.js'; export * from './session.js';
export * from './debug.js'; export * from './debug.js';
export * from './worktree.js';

View File

@ -1,154 +0,0 @@
/**
* Git worktree management utilities for takt
*/
import { execFileSync } from 'node:child_process';
import { join, resolve } from 'node:path';
import { existsSync, mkdirSync } from 'node:fs';
import { createLogger } from './debug.js';
const log = createLogger('worktree');
export interface WorktreeInfo {
path: string;
branch: string;
baseBranch: string;
}
/** Worktree configuration from Planner output */
export interface WorktreeConfig {
baseBranch: string;
branchName: string;
}
/**
* Parse worktree configuration from Planner output
*/
export function parseWorktreeConfig(content: string): WorktreeConfig | null {
// Match worktree: block with baseBranch and branchName
const worktreeMatch = content.match(/worktree:\s*\n\s*baseBranch:\s*(\S+)\s*\n\s*branchName:\s*(\S+)/);
if (worktreeMatch && worktreeMatch[1] && worktreeMatch[2]) {
return {
baseBranch: worktreeMatch[1],
branchName: worktreeMatch[2],
};
}
return null;
}
/**
* Generate a timestamp string for worktree directory
*/
export function generateTimestamp(): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
/**
* Sanitize branch name for use in directory name
*/
export function sanitizeBranchName(branchName: string): string {
return branchName
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 50);
}
/**
* Get the worktrees directory path
*/
export function getWorktreesDir(cwd: string): string {
return join(resolve(cwd), '.takt', 'worktrees');
}
/**
* Generate worktree path
*/
export function getWorktreePath(cwd: string, timestamp: string, branchName: string): string {
const sanitizedBranch = sanitizeBranchName(branchName);
return join(getWorktreesDir(cwd), `${timestamp}-${sanitizedBranch}`);
}
/**
* Create a new git worktree with a new branch
* @param cwd - Current working directory
* @param branchName - Name of the new branch to create
* @param baseBranch - Base branch to create the worktree from (required, determined by Planner)
*/
export function createWorktree(
cwd: string,
branchName: string,
baseBranch: string
): WorktreeInfo {
const timestamp = generateTimestamp();
const worktreePath = getWorktreePath(cwd, timestamp, branchName);
// Ensure worktrees directory exists
const worktreesDir = getWorktreesDir(cwd);
if (!existsSync(worktreesDir)) {
mkdirSync(worktreesDir, { recursive: true });
}
log.info('Creating worktree', { path: worktreePath, branch: branchName, baseBranch });
// Fetch latest from origin
try {
execFileSync('git', ['fetch', 'origin'], { cwd, stdio: 'pipe' });
} catch {
log.debug('Failed to fetch from origin, continuing with local state');
}
// Create worktree with new branch (using execFileSync to prevent command injection)
try {
const baseRef = `origin/${baseBranch}`;
execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, baseRef], {
cwd,
stdio: 'pipe',
});
} catch (e) {
// If origin/base doesn't exist, try local base
log.debug('Failed to create from origin, trying local branch', { error: e });
execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, baseBranch], {
cwd,
stdio: 'pipe',
});
}
log.info('Worktree created successfully', { path: worktreePath });
return {
path: worktreePath,
branch: branchName,
baseBranch,
};
}
/**
* Remove a worktree
*/
export function removeWorktree(cwd: string, worktreePath: string): void {
log.info('Removing worktree', { path: worktreePath });
execFileSync('git', ['worktree', 'remove', worktreePath, '--force'], { cwd, stdio: 'pipe' });
}
/**
* List all worktrees
*/
export function listWorktrees(cwd: string): string[] {
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd, encoding: 'utf-8' });
const paths: string[] = [];
for (const line of output.split('\n')) {
if (line.startsWith('worktree ')) {
paths.push(line.slice('worktree '.length));
}
}
return paths;
}

View File

@ -23,7 +23,6 @@ import {
addUserInput, addUserInput,
getPreviousOutput, getPreviousOutput,
} from './state-manager.js'; } from './state-manager.js';
import { parseWorktreeConfig } from '../utils/worktree.js';
import { generateReportDir } from '../utils/session.js'; import { generateReportDir } from '../utils/session.js';
// Re-export types for backward compatibility // Re-export types for backward compatibility
@ -35,7 +34,6 @@ export type {
IterationLimitCallback, IterationLimitCallback,
WorkflowEngineOptions, WorkflowEngineOptions,
} from './types.js'; } from './types.js';
export type { WorktreeConfig } from '../utils/worktree.js';
export { COMPLETE_STEP, ABORT_STEP } from './constants.js'; export { COMPLETE_STEP, ABORT_STEP } from './constants.js';
/** Workflow engine for orchestrating agent execution */ /** Workflow engine for orchestrating agent execution */
@ -103,12 +101,12 @@ export class WorkflowEngine extends EventEmitter {
addUserInput(this.state, input); addUserInput(this.state, input);
} }
/** Update working directory (used after worktree creation) */ /** Update working directory */
updateCwd(newCwd: string): void { updateCwd(newCwd: string): void {
this.cwd = newCwd; this.cwd = newCwd;
} }
/** Get current working directory (may be worktree path) */ /** Get current working directory */
getCwd(): string { getCwd(): string {
return this.cwd; return this.cwd;
} }
@ -217,14 +215,6 @@ export class WorkflowEngine extends EventEmitter {
const response = await this.runStep(step); const response = await this.runStep(step);
this.emit('step:complete', step, response); this.emit('step:complete', step, response);
// Check for worktree config in Planner output (when DONE)
if (step.name === 'plan' && response.status === 'done') {
const worktreeConfig = parseWorktreeConfig(response.content);
if (worktreeConfig) {
this.emit('planner:worktree_config', worktreeConfig);
}
}
if (response.status === 'blocked') { if (response.status === 'blocked') {
this.emit('step:blocked', step, response); this.emit('step:blocked', step, response);
const result = await handleBlocked(step, response, this.options); const result = await handleBlocked(step, response, this.options);

View File

@ -8,7 +8,6 @@
import type { WorkflowStep, AgentResponse, WorkflowState } from '../models/types.js'; import type { WorkflowStep, AgentResponse, WorkflowState } from '../models/types.js';
import type { StreamCallback } from '../agents/runner.js'; import type { StreamCallback } from '../agents/runner.js';
import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js'; import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js';
import type { WorktreeConfig } from '../utils/worktree.js';
/** Events emitted by workflow engine */ /** Events emitted by workflow engine */
export interface WorkflowEvents { export interface WorkflowEvents {
@ -20,7 +19,6 @@ export interface WorkflowEvents {
'workflow:abort': (state: WorkflowState, reason: string) => void; 'workflow:abort': (state: WorkflowState, reason: string) => void;
'iteration:limit': (iteration: number, maxIterations: number) => void; 'iteration:limit': (iteration: number, maxIterations: number) => void;
'step:loop_detected': (step: WorkflowStep, consecutiveCount: number) => void; 'step:loop_detected': (step: WorkflowStep, consecutiveCount: number) => void;
'planner:worktree_config': (config: WorktreeConfig) => void;
} }
/** User input request for blocked state */ /** User input request for blocked state */