refactor: sync with root をピースエンジンから単発エージェント呼び出しに変更
- executeTask(フルピースエンジン)→ Provider 抽象経由の単発エージェント呼び出しに簡素化 - Claude 固定の callClaudeCustom → getProvider() による Provider 抽象化 - permissionMode: 'full' → 'edit' + onPermissionRequest で Bash 自動承認 - コンフリクト解決プロンプトをテンプレートファイル(en/ja)に分離 - sync 後に worktree → project → origin の2段階プッシュを追加
This commit is contained in:
parent
a27c55420c
commit
9f15840d63
@ -11,6 +11,9 @@ vi.mock('node:child_process', () => ({
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
StreamDisplay: vi.fn(() => ({
|
||||
createHandler: vi.fn(() => vi.fn()),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', () => ({
|
||||
@ -22,37 +25,52 @@ vi.mock('../shared/utils/index.js', () => ({
|
||||
getErrorMessage: vi.fn((err) => String(err)),
|
||||
}));
|
||||
|
||||
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||
executeTask: vi.fn(),
|
||||
const mockAgentCall = vi.fn();
|
||||
|
||||
vi.mock('../infra/providers/index.js', () => ({
|
||||
getProvider: vi.fn(() => ({
|
||||
setup: vi.fn(() => ({ call: mockAgentCall })),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../features/tasks/execute/selectAndExecute.js', () => ({
|
||||
determinePiece: vi.fn(),
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
getLanguage: vi.fn(() => 'en'),
|
||||
resolveConfigValues: vi.fn(() => ({ provider: 'claude', model: 'sonnet' })),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/constants.js', () => ({
|
||||
DEFAULT_PIECE_NAME: 'default',
|
||||
vi.mock('../infra/github/index.js', () => ({
|
||||
pushBranch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/prompts/index.js', () => ({
|
||||
loadTemplate: vi.fn((_name: string, _lang: string, vars?: Record<string, string>) => {
|
||||
if (_name === 'sync_conflict_resolver_system_prompt') return 'system-prompt';
|
||||
if (_name === 'sync_conflict_resolver_message') return `message:${vars?.originalInstruction ?? ''}`;
|
||||
return '';
|
||||
}),
|
||||
}));
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { error as logError, success } from '../shared/ui/index.js';
|
||||
import { executeTask } from '../features/tasks/execute/taskExecution.js';
|
||||
import { determinePiece } from '../features/tasks/execute/selectAndExecute.js';
|
||||
import { pushBranch } from '../infra/github/index.js';
|
||||
import { getProvider } from '../infra/providers/index.js';
|
||||
import { syncBranchWithRoot } from '../features/tasks/list/taskSyncAction.js';
|
||||
import type { TaskListItem } from '../infra/task/index.js';
|
||||
import type { AgentResponse } from '../core/models/index.js';
|
||||
|
||||
const mockExistsSync = vi.mocked(fs.existsSync);
|
||||
const mockExecFileSync = vi.mocked(execFileSync);
|
||||
const mockExecuteTask = vi.mocked(executeTask);
|
||||
const mockDeterminePiece = vi.mocked(determinePiece);
|
||||
const mockLogError = vi.mocked(logError);
|
||||
const mockSuccess = vi.mocked(success);
|
||||
const mockPushBranch = vi.mocked(pushBranch);
|
||||
const mockGetProvider = vi.mocked(getProvider);
|
||||
|
||||
function makeTask(overrides: Partial<TaskListItem> = {}): TaskListItem {
|
||||
return {
|
||||
kind: 'completed',
|
||||
name: 'test-task',
|
||||
branch: 'task/test-task',
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
filePath: '/project/.takt/tasks.yaml',
|
||||
content: 'Implement feature X',
|
||||
@ -61,13 +79,23 @@ function makeTask(overrides: Partial<TaskListItem> = {}): TaskListItem {
|
||||
};
|
||||
}
|
||||
|
||||
function makeAgentResponse(overrides: Partial<AgentResponse> = {}): AgentResponse {
|
||||
return {
|
||||
persona: 'conflict-resolver',
|
||||
status: 'done',
|
||||
content: 'Conflicts resolved',
|
||||
timestamp: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const PROJECT_DIR = '/project';
|
||||
|
||||
describe('syncBranchWithRoot', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockDeterminePiece.mockResolvedValue('default');
|
||||
mockAgentCall.mockResolvedValue(makeAgentResponse());
|
||||
});
|
||||
|
||||
it('throws when called with a non-task BranchActionTarget', async () => {
|
||||
@ -113,90 +141,57 @@ describe('syncBranchWithRoot', () => {
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockLogError).toHaveBeenCalledWith(expect.stringContaining('Failed to fetch from root'));
|
||||
expect(mockExecuteTask).not.toHaveBeenCalled();
|
||||
expect(mockAgentCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns true and shows "Synced." when merge succeeds without conflicts', async () => {
|
||||
it('returns true and pushes when merge succeeds without conflicts', async () => {
|
||||
const task = makeTask();
|
||||
mockExecFileSync.mockReturnValue('' as never);
|
||||
|
||||
const result = await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Synced.');
|
||||
expect(mockExecuteTask).not.toHaveBeenCalled();
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Synced & pushed.');
|
||||
expect(mockAgentCall).not.toHaveBeenCalled();
|
||||
// worktree → project push
|
||||
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||
'git', ['push', PROJECT_DIR, 'HEAD'],
|
||||
expect.objectContaining({ cwd: task.worktreePath }),
|
||||
);
|
||||
// project → origin push
|
||||
expect(mockPushBranch).toHaveBeenCalledWith(PROJECT_DIR, 'task/test-task');
|
||||
});
|
||||
|
||||
it('calls executeTask with conflict resolution instruction when merge has conflicts', async () => {
|
||||
it('calls provider agent when merge has conflicts', async () => {
|
||||
const task = makeTask();
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); });
|
||||
|
||||
mockExecuteTask.mockResolvedValue(true);
|
||||
|
||||
const result = await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Conflicts resolved.');
|
||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Conflicts resolved & pushed.');
|
||||
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
||||
expect(mockAgentCall).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Implement feature X'),
|
||||
expect.objectContaining({
|
||||
cwd: task.worktreePath,
|
||||
projectCwd: PROJECT_DIR,
|
||||
pieceIdentifier: 'default',
|
||||
task: expect.stringContaining('Git merge has stopped due to merge conflicts.'),
|
||||
model: 'sonnet',
|
||||
permissionMode: 'edit',
|
||||
onPermissionRequest: expect.any(Function),
|
||||
onStream: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('includes original task content in conflict resolution instruction', async () => {
|
||||
const task = makeTask({ content: 'Implement feature X' });
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); });
|
||||
mockExecuteTask.mockResolvedValue(true);
|
||||
|
||||
await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
task: expect.stringContaining('Implement feature X'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses task piece when available for AI resolution', async () => {
|
||||
const task = makeTask({ data: { piece: 'custom-piece' } });
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); });
|
||||
mockDeterminePiece.mockResolvedValue('custom-piece');
|
||||
mockExecuteTask.mockResolvedValue(true);
|
||||
|
||||
await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
expect(mockDeterminePiece).toHaveBeenCalledWith(PROJECT_DIR, 'custom-piece');
|
||||
});
|
||||
|
||||
it('uses DEFAULT_PIECE_NAME when task.data.piece is not set', async () => {
|
||||
const task = makeTask({ data: undefined });
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); });
|
||||
mockExecuteTask.mockResolvedValue(true);
|
||||
|
||||
await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
expect(mockDeterminePiece).toHaveBeenCalledWith(PROJECT_DIR, 'default');
|
||||
});
|
||||
|
||||
it('aborts merge and returns false when AI resolution fails', async () => {
|
||||
const task = makeTask();
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); })
|
||||
.mockReturnValueOnce('' as never);
|
||||
mockExecuteTask.mockResolvedValue(false);
|
||||
mockAgentCall.mockResolvedValue(makeAgentResponse({ status: 'error' }));
|
||||
|
||||
const result = await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
@ -210,31 +205,13 @@ describe('syncBranchWithRoot', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('aborts merge and returns false when determinePiece returns null', async () => {
|
||||
const task = makeTask();
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); })
|
||||
.mockReturnValueOnce('' as never);
|
||||
mockDeterminePiece.mockResolvedValue(null);
|
||||
|
||||
const result = await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecuteTask).not.toHaveBeenCalled();
|
||||
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||
'git', ['merge', '--abort'],
|
||||
expect.objectContaining({ cwd: task.worktreePath }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when git merge --abort itself fails', async () => {
|
||||
const task = makeTask();
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); })
|
||||
.mockImplementationOnce(() => { throw new Error('abort failed'); });
|
||||
mockDeterminePiece.mockResolvedValue(null);
|
||||
mockAgentCall.mockResolvedValue(makeAgentResponse({ status: 'error' }));
|
||||
|
||||
const result = await syncBranchWithRoot(PROJECT_DIR, task);
|
||||
|
||||
@ -253,19 +230,4 @@ describe('syncBranchWithRoot', () => {
|
||||
expect.objectContaining({ cwd: task.worktreePath }),
|
||||
);
|
||||
});
|
||||
|
||||
it('passes agentOverrides to executeTask', async () => {
|
||||
const task = makeTask();
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce('' as never)
|
||||
.mockImplementationOnce(() => { throw new Error('CONFLICT'); });
|
||||
mockExecuteTask.mockResolvedValue(true);
|
||||
const options = { provider: 'anthropic' as never };
|
||||
|
||||
await syncBranchWithRoot(PROJECT_DIR, task, options);
|
||||
|
||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentOverrides: options }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -169,7 +169,7 @@ export async function listTasks(
|
||||
await instructBranch(cwd, task);
|
||||
break;
|
||||
case 'sync':
|
||||
await syncBranchWithRoot(cwd, task, options);
|
||||
await syncBranchWithRoot(cwd, task);
|
||||
break;
|
||||
case 'try':
|
||||
tryMergeBranch(cwd, task);
|
||||
|
||||
@ -1,36 +1,21 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { success, error as logError } from '../../../shared/ui/index.js';
|
||||
import { success, error as logError, StreamDisplay } from '../../../shared/ui/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
import { executeTask } from '../execute/taskExecution.js';
|
||||
import { determinePiece } from '../execute/selectAndExecute.js';
|
||||
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
|
||||
import { type BranchActionTarget, resolveTargetInstruction } from './taskActionTarget.js';
|
||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||
import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
|
||||
import { resolveConfigValues } from '../../../infra/config/index.js';
|
||||
import { pushBranch } from '../../../infra/github/index.js';
|
||||
import { loadTemplate } from '../../../shared/prompts/index.js';
|
||||
import { getLanguage } from '../../../infra/config/index.js';
|
||||
import { type BranchActionTarget, resolveTargetBranch, resolveTargetInstruction } from './taskActionTarget.js';
|
||||
|
||||
const log = createLogger('list-tasks');
|
||||
|
||||
const SYNC_REF = 'refs/remotes/root/sync-target';
|
||||
|
||||
function buildConflictResolutionInstruction(originalInstruction: string): string {
|
||||
return `Git merge has stopped due to merge conflicts.
|
||||
|
||||
Resolve all conflicts to complete the merge:
|
||||
1. Run \`git status\` to identify conflicted files
|
||||
2. For each conflicted file, resolve the conflict markers
|
||||
(<<<<<<< HEAD / ======= / >>>>>>> lines)
|
||||
Preserve changes that align with the original task intent
|
||||
3. Stage each resolved file: \`git add <file>\`
|
||||
4. Complete the merge: \`git commit\`
|
||||
|
||||
Original task:
|
||||
${originalInstruction}`;
|
||||
}
|
||||
|
||||
export async function syncBranchWithRoot(
|
||||
projectDir: string,
|
||||
target: BranchActionTarget,
|
||||
options?: TaskExecutionOptions,
|
||||
): Promise<boolean> {
|
||||
if (!('kind' in target)) {
|
||||
throw new Error('Sync requires a task target.');
|
||||
@ -57,41 +42,49 @@ export async function syncBranchWithRoot(
|
||||
return false;
|
||||
}
|
||||
|
||||
let mergeConflict = false;
|
||||
try {
|
||||
execFileSync('git', ['merge', SYNC_REF], {
|
||||
cwd: worktreePath,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
success('Synced.');
|
||||
log.info('Merge succeeded without conflicts', { worktreePath });
|
||||
return true;
|
||||
} catch (err) {
|
||||
mergeConflict = true;
|
||||
log.info('Merge conflict detected, attempting AI resolution', {
|
||||
worktreePath,
|
||||
error: getErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
||||
const pieceIdentifier = await determinePiece(projectDir, target.data?.piece ?? DEFAULT_PIECE_NAME);
|
||||
if (!pieceIdentifier) {
|
||||
abortMerge(worktreePath);
|
||||
return false;
|
||||
if (!mergeConflict) {
|
||||
pushSynced(worktreePath, projectDir, target);
|
||||
success('Synced & pushed.');
|
||||
log.info('Merge succeeded without conflicts', { worktreePath });
|
||||
return true;
|
||||
}
|
||||
|
||||
const lang = getLanguage();
|
||||
const originalInstruction = resolveTargetInstruction(target);
|
||||
const conflictInstruction = buildConflictResolutionInstruction(originalInstruction);
|
||||
const systemPrompt = loadTemplate('sync_conflict_resolver_system_prompt', lang);
|
||||
const prompt = loadTemplate('sync_conflict_resolver_message', lang, { originalInstruction });
|
||||
|
||||
const aiSuccess = await executeTask({
|
||||
task: conflictInstruction,
|
||||
const config = resolveConfigValues(projectDir, ['provider', 'model']);
|
||||
const providerType = (config.provider ?? 'claude') as ProviderType;
|
||||
const provider = getProvider(providerType);
|
||||
const agent = provider.setup({ name: 'conflict-resolver', systemPrompt });
|
||||
|
||||
const response = await agent.call(prompt, {
|
||||
cwd: worktreePath,
|
||||
pieceIdentifier,
|
||||
projectCwd: projectDir,
|
||||
agentOverrides: options,
|
||||
model: config.model,
|
||||
permissionMode: 'edit',
|
||||
onPermissionRequest: autoApproveBash,
|
||||
onStream: new StreamDisplay('conflict-resolver', false).createHandler(),
|
||||
});
|
||||
|
||||
if (aiSuccess) {
|
||||
success('Conflicts resolved.');
|
||||
if (response.status === 'done') {
|
||||
pushSynced(worktreePath, projectDir, target);
|
||||
success('Conflicts resolved & pushed.');
|
||||
log.info('AI conflict resolution succeeded', { worktreePath });
|
||||
return true;
|
||||
}
|
||||
@ -101,6 +94,25 @@ export async function syncBranchWithRoot(
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Auto-approve all tool invocations (agent runs in isolated worktree) */
|
||||
async function autoApproveBash(request: { toolName: string; input: Record<string, unknown> }) {
|
||||
return { behavior: 'allow' as const, updatedInput: request.input };
|
||||
}
|
||||
|
||||
/** Push worktree → project dir, then project dir → origin */
|
||||
function pushSynced(worktreePath: string, projectDir: string, target: BranchActionTarget): void {
|
||||
execFileSync('git', ['push', projectDir, 'HEAD'], {
|
||||
cwd: worktreePath,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
log.info('Pushed to main repo', { worktreePath, projectDir });
|
||||
|
||||
const branch = resolveTargetBranch(target);
|
||||
pushBranch(projectDir, branch);
|
||||
log.info('Pushed to origin', { projectDir, branch });
|
||||
}
|
||||
|
||||
function abortMerge(worktreePath: string): void {
|
||||
try {
|
||||
execFileSync('git', ['merge', '--abort'], {
|
||||
@ -110,6 +122,7 @@ function abortMerge(worktreePath: string): void {
|
||||
});
|
||||
log.info('git merge --abort completed', { worktreePath });
|
||||
} catch (err) {
|
||||
logError(`Failed to abort merge: ${getErrorMessage(err)}`);
|
||||
log.error('git merge --abort failed', { worktreePath, error: getErrorMessage(err) });
|
||||
}
|
||||
}
|
||||
|
||||
51
src/shared/prompts/en/sync_conflict_resolver_message.md
Normal file
51
src/shared/prompts/en/sync_conflict_resolver_message.md
Normal file
@ -0,0 +1,51 @@
|
||||
<!--
|
||||
template: sync_conflict_resolver_message
|
||||
role: user message for sync conflict resolver agent
|
||||
vars: originalInstruction
|
||||
caller: features/tasks/list/taskSyncAction.ts
|
||||
-->
|
||||
Git merge has stopped due to merge conflicts.
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Identify conflicts
|
||||
|
||||
Run `git status` to list unmerged files.
|
||||
|
||||
### 2. Understand context
|
||||
|
||||
Run these in parallel:
|
||||
- `git log --oneline HEAD -5` to see recent HEAD changes
|
||||
- `git log --oneline MERGE_HEAD -5` to see incoming changes (if merge)
|
||||
|
||||
### 3. Analyze each conflicted file
|
||||
|
||||
For each file:
|
||||
1. Read the full file (with conflict markers)
|
||||
2. For each conflict block (`<<<<<<<` to `>>>>>>>`):
|
||||
- Read HEAD side content
|
||||
- Read theirs side content
|
||||
- Determine what the diff means (version bump? refactor? feature addition?)
|
||||
- If unclear, check `git log --oneline -- {file}`
|
||||
3. Write your judgment before resolving
|
||||
|
||||
### 4. Resolve
|
||||
|
||||
- If one side is clearly correct: `git checkout --ours {file}` or `git checkout --theirs {file}`
|
||||
- If both changes need merging: edit the file to combine both sides
|
||||
- Stage each resolved file: `git add {file}`
|
||||
|
||||
After resolving, search for `<<<<<<<` to ensure no markers remain.
|
||||
|
||||
### 5. Verify
|
||||
|
||||
- Run build/test if available
|
||||
- Check that non-conflicted files are consistent with the resolution
|
||||
|
||||
### 6. Complete the merge
|
||||
|
||||
Run `git commit` to finalize.
|
||||
|
||||
## Original task
|
||||
|
||||
{{originalInstruction}}
|
||||
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
template: sync_conflict_resolver_system_prompt
|
||||
role: system prompt for sync conflict resolver agent
|
||||
vars: (none)
|
||||
caller: features/tasks/list/taskSyncAction.ts
|
||||
-->
|
||||
You are a git merge conflict resolver.
|
||||
|
||||
Your only job is to resolve merge conflicts and complete the merge commit.
|
||||
Do not refactor, improve, or change any code beyond what is necessary to resolve conflicts.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Read diffs before resolving.** Never apply `--ours` / `--theirs` without inspecting file contents.
|
||||
- **Do not blindly favor one side.** Even if one branch is "newer", check whether the other side has intentional changes.
|
||||
- **Document your reasoning.** For each conflict, note what each side contains and why you chose the resolution.
|
||||
- **Verify ripple effects.** After resolving, check that non-conflicted files are still consistent.
|
||||
|
||||
## Prohibited
|
||||
|
||||
- Resolving all files with `git checkout --ours .` or `git checkout --theirs .` without analysis
|
||||
- Leaving conflict markers (`<<<<<<<`) in any file
|
||||
- Running `git merge --abort` without user confirmation
|
||||
51
src/shared/prompts/ja/sync_conflict_resolver_message.md
Normal file
51
src/shared/prompts/ja/sync_conflict_resolver_message.md
Normal file
@ -0,0 +1,51 @@
|
||||
<!--
|
||||
template: sync_conflict_resolver_message
|
||||
role: sync コンフリクト解決エージェントのユーザーメッセージ
|
||||
vars: originalInstruction
|
||||
caller: features/tasks/list/taskSyncAction.ts
|
||||
-->
|
||||
Git merge がコンフリクトにより停止しました。
|
||||
|
||||
## 手順
|
||||
|
||||
### 1. コンフリクト状態を確認する
|
||||
|
||||
`git status` を実行して未マージファイルを列挙する。
|
||||
|
||||
### 2. コンテキストを把握する
|
||||
|
||||
以下を並列で実行:
|
||||
- `git log --oneline HEAD -5` で HEAD 側の最近の変更を確認
|
||||
- `git log --oneline MERGE_HEAD -5` で取り込み側の最近の変更を確認(merge の場合)
|
||||
|
||||
### 3. 各ファイルを分析する
|
||||
|
||||
ファイルごとに以下を実行:
|
||||
1. ファイル全体を読む(コンフリクトマーカー付きの状態)
|
||||
2. 各コンフリクトブロック(`<<<<<<<` 〜 `>>>>>>>`)について:
|
||||
- HEAD 側の内容を具体的に読む
|
||||
- theirs 側の内容を具体的に読む
|
||||
- 差分が何を意味するか分析する(バージョン番号?リファクタ?機能追加?)
|
||||
- 判断に迷う場合は `git log --oneline -- {file}` で変更履歴を確認する
|
||||
3. 解決前に判断根拠を記述する
|
||||
|
||||
### 4. 解決を実施する
|
||||
|
||||
- 片方採用が明確な場合: `git checkout --ours {file}` / `git checkout --theirs {file}`
|
||||
- 両方の変更を統合する場合: ファイルを編集してコンフリクトマーカーを除去し、両方の内容を結合する
|
||||
- 解決したファイルを `git add {file}` でマークする
|
||||
|
||||
解決後、`<<<<<<<` を検索してマーカーの取り残しがないか確認する。
|
||||
|
||||
### 5. 波及影響を確認する
|
||||
|
||||
- ビルド・テストが利用可能なら実行する
|
||||
- コンフリクト対象外のファイルが、解決した変更と矛盾していないか確認する
|
||||
|
||||
### 6. マージを完了する
|
||||
|
||||
`git commit` を実行して完了する。
|
||||
|
||||
## 元のタスク
|
||||
|
||||
{{originalInstruction}}
|
||||
@ -0,0 +1,24 @@
|
||||
<!--
|
||||
template: sync_conflict_resolver_system_prompt
|
||||
role: sync コンフリクト解決エージェントのシステムプロンプト
|
||||
vars: (none)
|
||||
caller: features/tasks/list/taskSyncAction.ts
|
||||
-->
|
||||
あなたは git merge コンフリクトの解決担当です。
|
||||
|
||||
コンフリクトを解決してマージコミットを完了することだけが仕事です。
|
||||
コンフリクト解決に必要な範囲を超えて、コードのリファクタリングや改善を行わないでください。
|
||||
|
||||
## 原則
|
||||
|
||||
- **差分を読まずに解決しない。** ファイルの中身を確認せずに `--ours` / `--theirs` を適用しない
|
||||
- **「〇〇優先」を盲従しない。** 片方が新しいからという理由だけで判断せず、もう一方に独自の意図がないか必ず確認する
|
||||
- **判断根拠を省略しない。** 各コンフリクトに「何が・なぜ・どちらを」の3点を書く
|
||||
- **波及を確認する。** コンフリクト対象外ファイルもビルド・テストで検証する
|
||||
|
||||
## 禁止事項
|
||||
|
||||
- 分析なしで `git checkout --ours .` / `git checkout --theirs .` を実行しない
|
||||
- 「とりあえず片方」で全ファイルを一括解決しない
|
||||
- コンフリクトマーカー (`<<<<<<<`) が残ったままにしない
|
||||
- `git merge --abort` をユーザーの確認なしに実行しない
|
||||
Loading…
x
Reference in New Issue
Block a user