| 作成 | src/task/git.ts | stageAndCommit() 共通関数。git commit ロジックのDRY化 |
| 作成 | `src/workflow/instruction-context.ts` | `instruction-builder.ts` からコンテキスト組立ロジック抽出 | | 作成 | `src/workflow/status-rules.ts` | `instruction-builder.ts` からステータスルールロジック抽出 | | 変更 | 35ファイル | `getErrorMessage()` 統一、`projectCwd` required 化、`process.cwd()` デフォルト除去、`sacrificeMode` 削除、`loadGlobalConfig` キャッシュ、`console.log` → `blankLine()`、`executeTask` options object 化 | resolved #44
This commit is contained in:
parent
1ee73c525c
commit
d6ac71f0e6
@ -32,7 +32,7 @@ vi.mock('../config/paths.js', async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Import after mocking
|
// Import after mocking
|
||||||
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey } = await import('../config/globalConfig.js');
|
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, invalidateGlobalConfigCache } = await import('../config/globalConfig.js');
|
||||||
|
|
||||||
describe('GlobalConfigSchema API key fields', () => {
|
describe('GlobalConfigSchema API key fields', () => {
|
||||||
it('should accept config without API keys', () => {
|
it('should accept config without API keys', () => {
|
||||||
@ -72,6 +72,7 @@ describe('GlobalConfigSchema API key fields', () => {
|
|||||||
|
|
||||||
describe('GlobalConfig load/save with API keys', () => {
|
describe('GlobalConfig load/save with API keys', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
mkdirSync(taktDir, { recursive: true });
|
mkdirSync(taktDir, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,6 +156,7 @@ describe('resolveAnthropicApiKey', () => {
|
|||||||
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
|
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
mkdirSync(taktDir, { recursive: true });
|
mkdirSync(taktDir, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -228,6 +230,7 @@ describe('resolveOpenaiApiKey', () => {
|
|||||||
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
|
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
mkdirSync(taktDir, { recursive: true });
|
mkdirSync(taktDir, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -200,22 +200,22 @@ steps:
|
|||||||
|
|
||||||
describe('loadWorkflow (builtin fallback)', () => {
|
describe('loadWorkflow (builtin fallback)', () => {
|
||||||
it('should load builtin workflow when user workflow does not exist', () => {
|
it('should load builtin workflow when user workflow does not exist', () => {
|
||||||
const workflow = loadWorkflow('default');
|
const workflow = loadWorkflow('default', process.cwd());
|
||||||
expect(workflow).not.toBeNull();
|
expect(workflow).not.toBeNull();
|
||||||
expect(workflow!.name).toBe('default');
|
expect(workflow!.name).toBe('default');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent workflow', () => {
|
it('should return null for non-existent workflow', () => {
|
||||||
const workflow = loadWorkflow('does-not-exist');
|
const workflow = loadWorkflow('does-not-exist', process.cwd());
|
||||||
expect(workflow).toBeNull();
|
expect(workflow).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load builtin workflows like simple, research', () => {
|
it('should load builtin workflows like simple, research', () => {
|
||||||
const simple = loadWorkflow('simple');
|
const simple = loadWorkflow('simple', process.cwd());
|
||||||
expect(simple).not.toBeNull();
|
expect(simple).not.toBeNull();
|
||||||
expect(simple!.name).toBe('simple');
|
expect(simple!.name).toBe('simple');
|
||||||
|
|
||||||
const research = loadWorkflow('research');
|
const research = loadWorkflow('research', process.cwd());
|
||||||
expect(research).not.toBeNull();
|
expect(research).not.toBeNull();
|
||||||
expect(research!.name).toBe('research');
|
expect(research!.name).toBe('research');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -90,7 +90,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
describe('abort() before run loop iteration', () => {
|
describe('abort() before run loop iteration', () => {
|
||||||
it('should abort immediately when abort() called before step execution', async () => {
|
it('should abort immediately when abort() called before step execution', async () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
const abortFn = vi.fn();
|
const abortFn = vi.fn();
|
||||||
engine.on('workflow:abort', abortFn);
|
engine.on('workflow:abort', abortFn);
|
||||||
@ -112,7 +112,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
describe('abort() during step execution', () => {
|
describe('abort() during step execution', () => {
|
||||||
it('should abort when abort() is called during runAgent', async () => {
|
it('should abort when abort() is called during runAgent', async () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
// Simulate abort during step execution: runAgent rejects after abort() is called
|
// Simulate abort during step execution: runAgent rejects after abort() is called
|
||||||
vi.mocked(runAgent).mockImplementation(async () => {
|
vi.mocked(runAgent).mockImplementation(async () => {
|
||||||
@ -135,7 +135,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
describe('abort() calls interruptAllQueries', () => {
|
describe('abort() calls interruptAllQueries', () => {
|
||||||
it('should call interruptAllQueries when abort() is called', () => {
|
it('should call interruptAllQueries when abort() is called', () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
engine.abort();
|
engine.abort();
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
describe('abort() idempotency', () => {
|
describe('abort() idempotency', () => {
|
||||||
it('should only call interruptAllQueries once on multiple abort() calls', () => {
|
it('should only call interruptAllQueries once on multiple abort() calls', () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
engine.abort();
|
engine.abort();
|
||||||
engine.abort();
|
engine.abort();
|
||||||
@ -159,14 +159,14 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
describe('isAbortRequested()', () => {
|
describe('isAbortRequested()', () => {
|
||||||
it('should return false initially', () => {
|
it('should return false initially', () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
expect(engine.isAbortRequested()).toBe(false);
|
expect(engine.isAbortRequested()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true after abort()', () => {
|
it('should return true after abort()', () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
engine.abort();
|
engine.abort();
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
describe('abort between steps', () => {
|
describe('abort between steps', () => {
|
||||||
it('should stop after completing current step when abort() is called', async () => {
|
it('should stop after completing current step when abort() is called', async () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
// First step completes normally, but abort is called during it
|
// First step completes normally, but abort is called during it
|
||||||
vi.mocked(runAgent).mockImplementation(async () => {
|
vi.mocked(runAgent).mockImplementation(async () => {
|
||||||
|
|||||||
@ -62,6 +62,7 @@ describe('WorkflowEngine agent overrides', () => {
|
|||||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, '/tmp/project', 'override task', {
|
const engine = new WorkflowEngine(config, '/tmp/project', 'override task', {
|
||||||
|
projectCwd: '/tmp/project',
|
||||||
provider: 'codex',
|
provider: 'codex',
|
||||||
model: 'cli-model',
|
model: 'cli-model',
|
||||||
});
|
});
|
||||||
@ -90,6 +91,7 @@ describe('WorkflowEngine agent overrides', () => {
|
|||||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, '/tmp/project', 'override task', {
|
const engine = new WorkflowEngine(config, '/tmp/project', 'override task', {
|
||||||
|
projectCwd: '/tmp/project',
|
||||||
provider: 'codex',
|
provider: 'codex',
|
||||||
model: 'cli-model',
|
model: 'cli-model',
|
||||||
});
|
});
|
||||||
@ -119,7 +121,7 @@ describe('WorkflowEngine agent overrides', () => {
|
|||||||
]);
|
]);
|
||||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, '/tmp/project', 'step task');
|
const engine = new WorkflowEngine(config, '/tmp/project', 'step task', { projectCwd: '/tmp/project' });
|
||||||
await engine.run();
|
await engine.run();
|
||||||
|
|
||||||
const options = vi.mocked(runAgent).mock.calls[0][2];
|
const options = vi.mocked(runAgent).mock.calls[0][2];
|
||||||
|
|||||||
@ -59,7 +59,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => {
|
|||||||
|
|
||||||
it('should abort when blocked and no onUserInput callback', async () => {
|
it('should abort when blocked and no onUserInput callback', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', status: 'blocked', content: 'Need clarification' }),
|
makeResponse({ agent: 'plan', status: 'blocked', content: 'Need clarification' }),
|
||||||
@ -84,7 +84,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => {
|
|||||||
it('should abort when blocked and onUserInput returns null', async () => {
|
it('should abort when blocked and onUserInput returns null', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const onUserInput = vi.fn().mockResolvedValue(null);
|
const onUserInput = vi.fn().mockResolvedValue(null);
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task', { onUserInput });
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onUserInput });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', status: 'blocked', content: 'Need info' }),
|
makeResponse({ agent: 'plan', status: 'blocked', content: 'Need info' }),
|
||||||
@ -103,7 +103,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => {
|
|||||||
it('should continue when blocked and onUserInput provides input', async () => {
|
it('should continue when blocked and onUserInput provides input', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const onUserInput = vi.fn().mockResolvedValueOnce('User provided clarification');
|
const onUserInput = vi.fn().mockResolvedValueOnce('User provided clarification');
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task', { onUserInput });
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onUserInput });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
// First: plan is blocked
|
// First: plan is blocked
|
||||||
|
|||||||
@ -68,7 +68,7 @@ describe('WorkflowEngine Integration: Error Handling', () => {
|
|||||||
describe('No rule matched', () => {
|
describe('No rule matched', () => {
|
||||||
it('should abort when detectMatchedRule returns undefined', async () => {
|
it('should abort when detectMatchedRule returns undefined', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Unclear output' }),
|
makeResponse({ agent: 'plan', content: 'Unclear output' }),
|
||||||
@ -94,7 +94,7 @@ describe('WorkflowEngine Integration: Error Handling', () => {
|
|||||||
describe('runAgent throws', () => {
|
describe('runAgent throws', () => {
|
||||||
it('should abort when runAgent throws an error', async () => {
|
it('should abort when runAgent throws an error', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
vi.mocked(runAgent).mockRejectedValueOnce(new Error('API connection failed'));
|
vi.mocked(runAgent).mockRejectedValueOnce(new Error('API connection failed'));
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ describe('WorkflowEngine Integration: Error Handling', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
vi.mocked(runAgent).mockResolvedValueOnce(
|
vi.mocked(runAgent).mockResolvedValueOnce(
|
||||||
@ -156,7 +156,7 @@ describe('WorkflowEngine Integration: Error Handling', () => {
|
|||||||
describe('Iteration limit', () => {
|
describe('Iteration limit', () => {
|
||||||
it('should abort when max iterations reached without onIterationLimit callback', async () => {
|
it('should abort when max iterations reached without onIterationLimit callback', async () => {
|
||||||
const config = buildDefaultWorkflowConfig({ maxIterations: 2 });
|
const config = buildDefaultWorkflowConfig({ maxIterations: 2 });
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||||
@ -190,6 +190,7 @@ describe('WorkflowEngine Integration: Error Handling', () => {
|
|||||||
const onIterationLimit = vi.fn().mockResolvedValueOnce(10);
|
const onIterationLimit = vi.fn().mockResolvedValueOnce(10);
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task', {
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', {
|
||||||
|
projectCwd: tmpDir,
|
||||||
onIterationLimit,
|
onIterationLimit,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -71,7 +71,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
describe('Happy path', () => {
|
describe('Happy path', () => {
|
||||||
it('should complete: plan → implement → ai_review → reviewers(all approved) → supervise → COMPLETE', async () => {
|
it('should complete: plan → implement → ai_review → reviewers(all approved) → supervise → COMPLETE', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan complete' }),
|
makeResponse({ agent: 'plan', content: 'Plan complete' }),
|
||||||
@ -110,7 +110,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
describe('Review reject and fix loop', () => {
|
describe('Review reject and fix loop', () => {
|
||||||
it('should handle: reviewers(needs_fix) → fix → reviewers(all approved) → supervise → COMPLETE', async () => {
|
it('should handle: reviewers(needs_fix) → fix → reviewers(all approved) → supervise → COMPLETE', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||||
@ -156,7 +156,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
describe('AI review reject and fix', () => {
|
describe('AI review reject and fix', () => {
|
||||||
it('should handle: ai_review(issues) → ai_fix → reviewers → supervise → COMPLETE', async () => {
|
it('should handle: ai_review(issues) → ai_fix → reviewers → supervise → COMPLETE', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||||
@ -193,7 +193,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
describe('ABORT transition', () => {
|
describe('ABORT transition', () => {
|
||||||
it('should abort when step transitions to ABORT', async () => {
|
it('should abort when step transitions to ABORT', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Requirements unclear' }),
|
makeResponse({ agent: 'plan', content: 'Requirements unclear' }),
|
||||||
@ -220,7 +220,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
describe('Event emissions', () => {
|
describe('Event emissions', () => {
|
||||||
it('should emit step:start and step:complete for each step', async () => {
|
it('should emit step:start and step:complete for each step', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||||
@ -267,7 +267,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const engine = new WorkflowEngine(simpleConfig, tmpDir, 'test task');
|
const engine = new WorkflowEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||||
@ -290,7 +290,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
|
|
||||||
it('should pass empty instruction to step:start for parallel steps', async () => {
|
it('should pass empty instruction to step:start for parallel steps', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||||
@ -328,7 +328,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
|
|
||||||
it('should emit iteration:limit when max iterations reached', async () => {
|
it('should emit iteration:limit when max iterations reached', async () => {
|
||||||
const config = buildDefaultWorkflowConfig({ maxIterations: 1 });
|
const config = buildDefaultWorkflowConfig({ maxIterations: 1 });
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||||
@ -352,7 +352,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
describe('Step output tracking', () => {
|
describe('Step output tracking', () => {
|
||||||
it('should store outputs for all executed steps', async () => {
|
it('should store outputs for all executed steps', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan output' }),
|
makeResponse({ agent: 'plan', content: 'Plan output' }),
|
||||||
@ -390,7 +390,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
const config = buildDefaultWorkflowConfig({ initialStep: 'nonexistent' });
|
const config = buildDefaultWorkflowConfig({ initialStep: 'nonexistent' });
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new WorkflowEngine(config, tmpDir, 'test task');
|
new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
}).toThrow('Unknown step: nonexistent');
|
}).toThrow('Unknown step: nonexistent');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -407,7 +407,7 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new WorkflowEngine(config, tmpDir, 'test task');
|
new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
}).toThrow('nonexistent_step');
|
}).toThrow('nonexistent_step');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -60,7 +60,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => {
|
|||||||
|
|
||||||
it('should aggregate sub-step outputs with ## headers and --- separators', async () => {
|
it('should aggregate sub-step outputs with ## headers and --- separators', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||||
@ -97,7 +97,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => {
|
|||||||
|
|
||||||
it('should store individual sub-step outputs in stepOutputs', async () => {
|
it('should store individual sub-step outputs in stepOutputs', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||||
@ -129,7 +129,7 @@ describe('WorkflowEngine Integration: Parallel Step Aggregation', () => {
|
|||||||
|
|
||||||
it('should execute sub-steps concurrently (both runAgent calls happen)', async () => {
|
it('should execute sub-steps concurrently (both runAgent calls happen)', async () => {
|
||||||
const config = buildDefaultWorkflowConfig();
|
const config = buildDefaultWorkflowConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
mockRunAgentSequence([
|
mockRunAgentSequence([
|
||||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||||
|
|||||||
@ -20,11 +20,12 @@ vi.mock('node:os', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Import after mocks are set up
|
// Import after mocks are set up
|
||||||
const { loadGlobalConfig, saveGlobalConfig } = await import('../config/globalConfig.js');
|
const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache } = await import('../config/globalConfig.js');
|
||||||
const { getGlobalConfigPath } = await import('../config/paths.js');
|
const { getGlobalConfigPath } = await import('../config/paths.js');
|
||||||
|
|
||||||
describe('loadGlobalConfig', () => {
|
describe('loadGlobalConfig', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
mkdirSync(testHomeDir, { recursive: true });
|
mkdirSync(testHomeDir, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,12 +48,20 @@ describe('loadGlobalConfig', () => {
|
|||||||
expect(config.pipeline).toBeUndefined();
|
expect(config.pipeline).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a fresh copy each time (no shared reference)', () => {
|
it('should return the same cached object on subsequent calls', () => {
|
||||||
const config1 = loadGlobalConfig();
|
const config1 = loadGlobalConfig();
|
||||||
const config2 = loadGlobalConfig();
|
const config2 = loadGlobalConfig();
|
||||||
|
|
||||||
config1.trustedDirectories.push('/tmp/test');
|
expect(config1).toBe(config2);
|
||||||
expect(config2.trustedDirectories).toEqual([]);
|
});
|
||||||
|
|
||||||
|
it('should return a fresh object after cache invalidation', () => {
|
||||||
|
const config1 = loadGlobalConfig();
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
const config2 = loadGlobalConfig();
|
||||||
|
|
||||||
|
expect(config1).not.toBe(config2);
|
||||||
|
expect(config1).toEqual(config2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load from config.yaml when it exists', () => {
|
it('should load from config.yaml when it exists', () => {
|
||||||
@ -105,10 +114,41 @@ describe('loadGlobalConfig', () => {
|
|||||||
commitMessageTemplate: 'feat: {title} (#{issue})',
|
commitMessageTemplate: 'feat: {title} (#{issue})',
|
||||||
};
|
};
|
||||||
saveGlobalConfig(config);
|
saveGlobalConfig(config);
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
const reloaded = loadGlobalConfig();
|
const reloaded = loadGlobalConfig();
|
||||||
expect(reloaded.pipeline).toBeDefined();
|
expect(reloaded.pipeline).toBeDefined();
|
||||||
expect(reloaded.pipeline!.defaultBranchPrefix).toBe('takt/');
|
expect(reloaded.pipeline!.defaultBranchPrefix).toBe('takt/');
|
||||||
expect(reloaded.pipeline!.commitMessageTemplate).toBe('feat: {title} (#{issue})');
|
expect(reloaded.pipeline!.commitMessageTemplate).toBe('feat: {title} (#{issue})');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should read from cache without hitting disk on second call', () => {
|
||||||
|
const taktDir = join(testHomeDir, '.takt');
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
getGlobalConfigPath(),
|
||||||
|
'language: ja\nprovider: codex\n',
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const config1 = loadGlobalConfig();
|
||||||
|
expect(config1.language).toBe('ja');
|
||||||
|
|
||||||
|
// Overwrite file on disk - cached result should still be returned
|
||||||
|
writeFileSync(
|
||||||
|
getGlobalConfigPath(),
|
||||||
|
'language: en\nprovider: claude\n',
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const config2 = loadGlobalConfig();
|
||||||
|
expect(config2.language).toBe('ja');
|
||||||
|
expect(config2).toBe(config1);
|
||||||
|
|
||||||
|
// After invalidation, the new file content is read
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
const config3 = loadGlobalConfig();
|
||||||
|
expect(config3.language).toBe('en');
|
||||||
|
expect(config3).not.toBe(config1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,6 +27,8 @@ vi.mock('../config/paths.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../utils/ui.js', () => ({
|
vi.mock('../utils/ui.js', () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
blankLine: vi.fn(),
|
||||||
StreamDisplay: vi.fn().mockImplementation(() => ({
|
StreamDisplay: vi.fn().mockImplementation(() => ({
|
||||||
createHandler: vi.fn(() => vi.fn()),
|
createHandler: vi.fn(() => vi.fn()),
|
||||||
flush: vi.fn(),
|
flush: vi.fn(),
|
||||||
|
|||||||
@ -49,6 +49,7 @@ vi.mock('../utils/ui.js', () => ({
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
status: vi.fn(),
|
status: vi.fn(),
|
||||||
|
blankLine: vi.fn(),
|
||||||
StreamDisplay: vi.fn().mockImplementation(() => ({
|
StreamDisplay: vi.fn().mockImplementation(() => ({
|
||||||
createHandler: () => vi.fn(),
|
createHandler: () => vi.fn(),
|
||||||
flush: vi.fn(),
|
flush: vi.fn(),
|
||||||
|
|||||||
@ -168,6 +168,7 @@ describe('Workflow Engine IT: Happy Path', () => {
|
|||||||
|
|
||||||
const config = buildSimpleWorkflow(agentPaths);
|
const config = buildSimpleWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
|
projectCwd: testDir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -184,6 +185,7 @@ describe('Workflow Engine IT: Happy Path', () => {
|
|||||||
|
|
||||||
const config = buildSimpleWorkflow(agentPaths);
|
const config = buildSimpleWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Vague task', {
|
const engine = new WorkflowEngine(config, testDir, 'Vague task', {
|
||||||
|
projectCwd: testDir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,6 +228,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
|
|||||||
|
|
||||||
const config = buildLoopWorkflow(agentPaths);
|
const config = buildLoopWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task needing fix', {
|
const engine = new WorkflowEngine(config, testDir, 'Task needing fix', {
|
||||||
|
projectCwd: testDir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -245,6 +248,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
|
|||||||
|
|
||||||
const config = buildLoopWorkflow(agentPaths);
|
const config = buildLoopWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Unfixable task', {
|
const engine = new WorkflowEngine(config, testDir, 'Unfixable task', {
|
||||||
|
projectCwd: testDir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -282,6 +286,7 @@ describe('Workflow Engine IT: Max Iterations', () => {
|
|||||||
config.maxIterations = 5;
|
config.maxIterations = 5;
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
||||||
|
projectCwd: testDir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -317,6 +322,7 @@ describe('Workflow Engine IT: Step Output Tracking', () => {
|
|||||||
|
|
||||||
const config = buildSimpleWorkflow(agentPaths);
|
const config = buildSimpleWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Track outputs', {
|
const engine = new WorkflowEngine(config, testDir, 'Track outputs', {
|
||||||
|
projectCwd: testDir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,7 @@ vi.mock('../utils/ui.js', () => ({
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
status: vi.fn(),
|
status: vi.fn(),
|
||||||
|
blankLine: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock debug logger
|
// Mock debug logger
|
||||||
@ -146,13 +147,13 @@ describe('executePipeline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
'Fix the bug',
|
task: 'Fix the bug',
|
||||||
'/tmp/test',
|
cwd: '/tmp/test',
|
||||||
'default',
|
workflowIdentifier: 'default',
|
||||||
'/tmp/test',
|
projectCwd: '/tmp/test',
|
||||||
undefined,
|
agentOverrides: undefined,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes provider/model overrides to task execution', async () => {
|
it('passes provider/model overrides to task execution', async () => {
|
||||||
@ -168,13 +169,13 @@ describe('executePipeline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
'Fix the bug',
|
task: 'Fix the bug',
|
||||||
'/tmp/test',
|
cwd: '/tmp/test',
|
||||||
'default',
|
workflowIdentifier: 'default',
|
||||||
'/tmp/test',
|
projectCwd: '/tmp/test',
|
||||||
{ provider: 'codex', model: 'codex-model' },
|
agentOverrides: { provider: 'codex', model: 'codex-model' },
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return exit code 5 when PR creation fails', async () => {
|
it('should return exit code 5 when PR creation fails', async () => {
|
||||||
@ -225,13 +226,13 @@ describe('executePipeline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
'From --task flag',
|
task: 'From --task flag',
|
||||||
'/tmp/test',
|
cwd: '/tmp/test',
|
||||||
'magi',
|
workflowIdentifier: 'magi',
|
||||||
'/tmp/test',
|
projectCwd: '/tmp/test',
|
||||||
undefined,
|
agentOverrides: undefined,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PipelineConfig template expansion', () => {
|
describe('PipelineConfig template expansion', () => {
|
||||||
@ -385,13 +386,13 @@ describe('executePipeline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
expect(mockExecuteTask).toHaveBeenCalledWith({
|
||||||
'Fix the bug',
|
task: 'Fix the bug',
|
||||||
'/tmp/test',
|
cwd: '/tmp/test',
|
||||||
'default',
|
workflowIdentifier: 'default',
|
||||||
'/tmp/test',
|
projectCwd: '/tmp/test',
|
||||||
undefined,
|
agentOverrides: undefined,
|
||||||
);
|
});
|
||||||
|
|
||||||
// No git operations should have been called
|
// No git operations should have been called
|
||||||
const gitCalls = mockExecFileSync.mock.calls.filter(
|
const gitCalls = mockExecFileSync.mock.calls.filter(
|
||||||
|
|||||||
@ -34,6 +34,7 @@ vi.mock('../utils/ui.js', () => ({
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
status: vi.fn(),
|
status: vi.fn(),
|
||||||
|
blankLine: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../utils/debug.js', () => ({
|
vi.mock('../utils/debug.js', () => ({
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { loadWorkflow } from '../config/loader.js';
|
import { loadWorkflow } from '../config/loader.js';
|
||||||
|
|
||||||
describe('expert workflow parallel structure', () => {
|
describe('expert workflow parallel structure', () => {
|
||||||
const workflow = loadWorkflow('expert');
|
const workflow = loadWorkflow('expert', process.cwd());
|
||||||
|
|
||||||
it('should load successfully', () => {
|
it('should load successfully', () => {
|
||||||
expect(workflow).not.toBeNull();
|
expect(workflow).not.toBeNull();
|
||||||
@ -95,7 +95,7 @@ describe('expert workflow parallel structure', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('expert-cqrs workflow parallel structure', () => {
|
describe('expert-cqrs workflow parallel structure', () => {
|
||||||
const workflow = loadWorkflow('expert-cqrs');
|
const workflow = loadWorkflow('expert-cqrs', process.cwd());
|
||||||
|
|
||||||
it('should load successfully', () => {
|
it('should load successfully', () => {
|
||||||
expect(workflow).not.toBeNull();
|
expect(workflow).not.toBeNull();
|
||||||
|
|||||||
@ -75,7 +75,7 @@ describe('loadWorkflowByIdentifier', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load workflow by name (builtin)', () => {
|
it('should load workflow by name (builtin)', () => {
|
||||||
const workflow = loadWorkflowByIdentifier('default');
|
const workflow = loadWorkflowByIdentifier('default', process.cwd());
|
||||||
expect(workflow).not.toBeNull();
|
expect(workflow).not.toBeNull();
|
||||||
expect(workflow!.name).toBe('default');
|
expect(workflow!.name).toBe('default');
|
||||||
});
|
});
|
||||||
@ -108,7 +108,7 @@ describe('loadWorkflowByIdentifier', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent name', () => {
|
it('should return null for non-existent name', () => {
|
||||||
const workflow = loadWorkflowByIdentifier('non-existent-workflow-xyz');
|
const workflow = loadWorkflowByIdentifier('non-existent-workflow-xyz', process.cwd());
|
||||||
expect(workflow).toBeNull();
|
expect(workflow).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
type PermissionMode,
|
type PermissionMode,
|
||||||
} from '@anthropic-ai/claude-agent-sdk';
|
} from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
import {
|
import {
|
||||||
generateQueryId,
|
generateQueryId,
|
||||||
registerQuery,
|
registerQuery,
|
||||||
@ -220,7 +221,7 @@ function handleQueryError(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
|
||||||
if (hasResultMessage && success) {
|
if (hasResultMessage && success) {
|
||||||
log.info('Claude query completed with post-completion error (ignoring)', {
|
log.info('Claude query completed with post-completion error (ignoring)', {
|
||||||
|
|||||||
13
src/cli.ts
13
src/cli.ts
@ -48,6 +48,7 @@ import { autoCommitAndPush } from './task/autoCommit.js';
|
|||||||
import { summarizeTaskName } from './task/summarize.js';
|
import { summarizeTaskName } from './task/summarize.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
||||||
import { checkForUpdates } from './utils/updateNotifier.js';
|
import { checkForUpdates } from './utils/updateNotifier.js';
|
||||||
|
import { getErrorMessage } from './utils/error.js';
|
||||||
import { resolveIssueTask, isIssueReference } from './github/issue.js';
|
import { resolveIssueTask, isIssueReference } from './github/issue.js';
|
||||||
import { createPullRequest, buildPrBody } from './github/pr.js';
|
import { createPullRequest, buildPrBody } from './github/pr.js';
|
||||||
import type { TaskExecutionOptions } from './commands/taskExecution.js';
|
import type { TaskExecutionOptions } from './commands/taskExecution.js';
|
||||||
@ -137,7 +138,13 @@ async function selectAndExecuteTask(
|
|||||||
);
|
);
|
||||||
|
|
||||||
log.info('Starting task execution', { workflow: workflowIdentifier, worktree: isWorktree });
|
log.info('Starting task execution', { workflow: workflowIdentifier, worktree: isWorktree });
|
||||||
const taskSuccess = await executeTask(task, execCwd, workflowIdentifier, cwd, agentOverrides);
|
const taskSuccess = await executeTask({
|
||||||
|
task,
|
||||||
|
cwd: execCwd,
|
||||||
|
workflowIdentifier,
|
||||||
|
projectCwd: cwd,
|
||||||
|
agentOverrides,
|
||||||
|
});
|
||||||
|
|
||||||
if (taskSuccess && isWorktree) {
|
if (taskSuccess && isWorktree) {
|
||||||
const commitResult = autoCommitAndPush(execCwd, task, cwd);
|
const commitResult = autoCommitAndPush(execCwd, task, cwd);
|
||||||
@ -449,7 +456,7 @@ program
|
|||||||
const resolvedTask = resolveIssueTask(`#${issueFromOption}`);
|
const resolvedTask = resolveIssueTask(`#${issueFromOption}`);
|
||||||
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
|
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e instanceof Error ? e.message : String(e));
|
error(getErrorMessage(e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -463,7 +470,7 @@ program
|
|||||||
info('Fetching GitHub Issue...');
|
info('Fetching GitHub Issue...');
|
||||||
resolvedTask = resolveIssueTask(task);
|
resolvedTask = resolveIssueTask(task);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e instanceof Error ? e.message : String(e));
|
error(getErrorMessage(e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Codex } from '@openai/codex-sdk';
|
|||||||
import type { AgentResponse, Status } from '../models/types.js';
|
import type { AgentResponse, Status } from '../models/types.js';
|
||||||
import type { StreamCallback } from '../claude/process.js';
|
import type { StreamCallback } from '../claude/process.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
|
|
||||||
const log = createLogger('codex-sdk');
|
const log = createLogger('codex-sdk');
|
||||||
|
|
||||||
@ -486,7 +487,7 @@ export async function callCodex(
|
|||||||
sessionId: threadId,
|
sessionId: threadId,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = getErrorMessage(error);
|
||||||
emitResult(options.onStream, false, message, threadId);
|
emitResult(options.onStream, false, message, threadId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { summarizeTaskName } from '../task/summarize.js';
|
|||||||
import { loadGlobalConfig } from '../config/globalConfig.js';
|
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||||
import { getProvider, type ProviderType } from '../providers/index.js';
|
import { getProvider, type ProviderType } from '../providers/index.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
import { listWorkflows } from '../config/workflowLoader.js';
|
import { listWorkflows } from '../config/workflowLoader.js';
|
||||||
import { getCurrentWorkflow } from '../config/paths.js';
|
import { getCurrentWorkflow } from '../config/paths.js';
|
||||||
import { interactiveMode } from './interactive.js';
|
import { interactiveMode } from './interactive.js';
|
||||||
@ -87,7 +88,7 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
taskContent = resolveIssueTask(task);
|
taskContent = resolveIssueTask(task);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = getErrorMessage(e);
|
||||||
log.error('Failed to fetch GitHub Issue', { task, error: msg });
|
log.error('Failed to fetch GitHub Issue', { task, error: msg });
|
||||||
info(`Failed to fetch issue ${task}: ${msg}`);
|
info(`Failed to fetch issue ${task}: ${msg}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -75,14 +75,9 @@ function getPermissionModeOptions(currentMode: PermissionMode): {
|
|||||||
*/
|
*/
|
||||||
export function getCurrentPermissionMode(cwd: string): PermissionMode {
|
export function getCurrentPermissionMode(cwd: string): PermissionMode {
|
||||||
const config = loadProjectConfig(cwd);
|
const config = loadProjectConfig(cwd);
|
||||||
// Support both old sacrificeMode boolean and new permissionMode string
|
|
||||||
if (config.permissionMode) {
|
if (config.permissionMode) {
|
||||||
return config.permissionMode as PermissionMode;
|
return config.permissionMode as PermissionMode;
|
||||||
}
|
}
|
||||||
// Legacy: convert sacrificeMode boolean to new format
|
|
||||||
if (config.sacrificeMode) {
|
|
||||||
return 'sacrifice-my-pc';
|
|
||||||
}
|
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,8 +86,6 @@ export function getCurrentPermissionMode(cwd: string): PermissionMode {
|
|||||||
*/
|
*/
|
||||||
export function setPermissionMode(cwd: string, mode: PermissionMode): void {
|
export function setPermissionMode(cwd: string, mode: PermissionMode): void {
|
||||||
updateProjectConfig(cwd, 'permissionMode', mode);
|
updateProjectConfig(cwd, 'permissionMode', mode);
|
||||||
// @deprecated TODO: Remove in v1.0 - legacy sacrificeMode for backwards compatibility
|
|
||||||
updateProjectConfig(cwd, 'sacrificeMode', mode === 'sacrifice-my-pc');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSy
|
|||||||
import { join, dirname } from 'node:path';
|
import { join, dirname } from 'node:path';
|
||||||
import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../config/paths.js';
|
import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../config/paths.js';
|
||||||
import { getLanguage } from '../config/globalConfig.js';
|
import { getLanguage } from '../config/globalConfig.js';
|
||||||
import { header, success, info, warn, error } from '../utils/ui.js';
|
import { header, success, info, warn, error, blankLine } from '../utils/ui.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eject a builtin workflow to user space for customization.
|
* Eject a builtin workflow to user space for customization.
|
||||||
@ -90,7 +90,7 @@ function listAvailableBuiltins(builtinWorkflowsDir: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info('Available builtin workflows:');
|
info('Available builtin workflows:');
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
for (const entry of readdirSync(builtinWorkflowsDir).sort()) {
|
for (const entry of readdirSync(builtinWorkflowsDir).sort()) {
|
||||||
if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue;
|
if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue;
|
||||||
@ -100,7 +100,7 @@ function listAvailableBuiltins(builtinWorkflowsDir: string): void {
|
|||||||
info(` ${name}`);
|
info(` ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
blankLine();
|
||||||
info('Usage: takt eject {name}');
|
info('Usage: takt eject {name}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,8 @@ import { isQuietMode } from '../cli.js';
|
|||||||
import { loadAgentSessions, updateAgentSession } from '../config/paths.js';
|
import { loadAgentSessions, updateAgentSession } from '../config/paths.js';
|
||||||
import { getProvider, type ProviderType } from '../providers/index.js';
|
import { getProvider, type ProviderType } from '../providers/index.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
import { info, StreamDisplay } from '../utils/ui.js';
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
|
import { info, error, blankLine, StreamDisplay } from '../utils/ui.js';
|
||||||
const log = createLogger('interactive');
|
const log = createLogger('interactive');
|
||||||
|
|
||||||
const INTERACTIVE_SYSTEM_PROMPT = `You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process.
|
const INTERACTIVE_SYSTEM_PROMPT = `You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process.
|
||||||
@ -148,7 +149,7 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
info('Resuming previous session');
|
info('Resuming previous session');
|
||||||
}
|
}
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
/** Call AI with automatic retry on session error (stale/invalid session ID). */
|
/** Call AI with automatic retry on session error (stale/invalid session ID). */
|
||||||
async function callAIWithRetry(prompt: string): Promise<CallAIResult | null> {
|
async function callAIWithRetry(prompt: string): Promise<CallAIResult | null> {
|
||||||
@ -173,10 +174,10 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = getErrorMessage(e);
|
||||||
log.error('AI call failed', { error: msg });
|
log.error('AI call failed', { error: msg });
|
||||||
console.log(chalk.red(`Error: ${msg}`));
|
error(msg);
|
||||||
console.log();
|
blankLine();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +190,7 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
const result = await callAIWithRetry(initialInput);
|
const result = await callAIWithRetry(initialInput);
|
||||||
if (result) {
|
if (result) {
|
||||||
history.push({ role: 'assistant', content: result.content });
|
history.push({ role: 'assistant', content: result.content });
|
||||||
console.log();
|
blankLine();
|
||||||
} else {
|
} else {
|
||||||
history.pop();
|
history.pop();
|
||||||
}
|
}
|
||||||
@ -200,7 +201,7 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
|
|
||||||
// EOF (Ctrl+D)
|
// EOF (Ctrl+D)
|
||||||
if (input === null) {
|
if (input === null) {
|
||||||
console.log();
|
blankLine();
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
return { confirmed: false, task: '' };
|
return { confirmed: false, task: '' };
|
||||||
}
|
}
|
||||||
@ -238,7 +239,7 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
const result = await callAIWithRetry(trimmed);
|
const result = await callAIWithRetry(trimmed);
|
||||||
if (result) {
|
if (result) {
|
||||||
history.push({ role: 'assistant', content: result.content });
|
history.push({ role: 'assistant', content: result.content });
|
||||||
console.log();
|
blankLine();
|
||||||
} else {
|
} else {
|
||||||
history.pop();
|
history.pop();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,9 @@ import {
|
|||||||
} from '../task/branchList.js';
|
} from '../task/branchList.js';
|
||||||
import { autoCommitAndPush } from '../task/autoCommit.js';
|
import { autoCommitAndPush } from '../task/autoCommit.js';
|
||||||
import { selectOption, confirm, promptInput } from '../prompt/index.js';
|
import { selectOption, confirm, promptInput } from '../prompt/index.js';
|
||||||
import { info, success, error as logError, warn } from '../utils/ui.js';
|
import { info, success, error as logError, warn, header, blankLine } from '../utils/ui.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
import { executeTask, type TaskExecutionOptions } from './taskExecution.js';
|
import { executeTask, type TaskExecutionOptions } from './taskExecution.js';
|
||||||
import { listWorkflows } from '../config/workflowLoader.js';
|
import { listWorkflows } from '../config/workflowLoader.js';
|
||||||
import { getCurrentWorkflow } from '../config/paths.js';
|
import { getCurrentWorkflow } from '../config/paths.js';
|
||||||
@ -80,12 +81,11 @@ async function showDiffAndPromptAction(
|
|||||||
defaultBranch: string,
|
defaultBranch: string,
|
||||||
item: BranchListItem,
|
item: BranchListItem,
|
||||||
): Promise<ListAction | null> {
|
): Promise<ListAction | null> {
|
||||||
console.log();
|
header(item.info.branch);
|
||||||
console.log(chalk.bold.cyan(`=== ${item.info.branch} ===`));
|
|
||||||
if (item.originalInstruction) {
|
if (item.originalInstruction) {
|
||||||
console.log(chalk.dim(` ${item.originalInstruction}`));
|
console.log(chalk.dim(` ${item.originalInstruction}`));
|
||||||
}
|
}
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
// Show diff stat
|
// Show diff stat
|
||||||
try {
|
try {
|
||||||
@ -132,7 +132,7 @@ export function tryMergeBranch(projectDir: string, item: BranchListItem): boolea
|
|||||||
log.info('Try-merge (squash) completed', { branch });
|
log.info('Try-merge (squash) completed', { branch });
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = getErrorMessage(err);
|
||||||
logError(`Squash merge failed: ${msg}`);
|
logError(`Squash merge failed: ${msg}`);
|
||||||
logError('You may need to resolve conflicts manually.');
|
logError('You may need to resolve conflicts manually.');
|
||||||
log.error('Try-merge (squash) failed', { branch, error: msg });
|
log.error('Try-merge (squash) failed', { branch, error: msg });
|
||||||
@ -180,7 +180,7 @@ export function mergeBranch(projectDir: string, item: BranchListItem): boolean {
|
|||||||
log.info('Branch merged & cleaned up', { branch, alreadyMerged });
|
log.info('Branch merged & cleaned up', { branch, alreadyMerged });
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = getErrorMessage(err);
|
||||||
logError(`Merge failed: ${msg}`);
|
logError(`Merge failed: ${msg}`);
|
||||||
logError('You may need to resolve conflicts manually.');
|
logError('You may need to resolve conflicts manually.');
|
||||||
log.error('Merge & cleanup failed', { branch, error: msg });
|
log.error('Merge & cleanup failed', { branch, error: msg });
|
||||||
@ -210,7 +210,7 @@ export function deleteBranch(projectDir: string, item: BranchListItem): boolean
|
|||||||
log.info('Branch deleted', { branch });
|
log.info('Branch deleted', { branch });
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = getErrorMessage(err);
|
||||||
logError(`Delete failed: ${msg}`);
|
logError(`Delete failed: ${msg}`);
|
||||||
log.error('Delete failed', { branch, error: msg });
|
log.error('Delete failed', { branch, error: msg });
|
||||||
return false;
|
return false;
|
||||||
@ -324,7 +324,13 @@ export async function instructBranch(
|
|||||||
: instruction;
|
: instruction;
|
||||||
|
|
||||||
// 5. Execute task on temp clone
|
// 5. Execute task on temp clone
|
||||||
const taskSuccess = await executeTask(fullInstruction, clone.path, selectedWorkflow, projectDir, options);
|
const taskSuccess = await executeTask({
|
||||||
|
task: fullInstruction,
|
||||||
|
cwd: clone.path,
|
||||||
|
workflowIdentifier: selectedWorkflow,
|
||||||
|
projectCwd: projectDir,
|
||||||
|
agentOverrides: options,
|
||||||
|
});
|
||||||
|
|
||||||
// 6. Auto-commit+push if successful
|
// 6. Auto-commit+push if successful
|
||||||
if (taskSuccess) {
|
if (taskSuccess) {
|
||||||
|
|||||||
@ -12,10 +12,12 @@
|
|||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { fetchIssue, formatIssueAsTask, checkGhCli, type GitHubIssue } from '../github/issue.js';
|
import { fetchIssue, formatIssueAsTask, checkGhCli, type GitHubIssue } from '../github/issue.js';
|
||||||
import { createPullRequest, pushBranch, buildPrBody } from '../github/pr.js';
|
import { createPullRequest, pushBranch, buildPrBody } from '../github/pr.js';
|
||||||
|
import { stageAndCommit } from '../task/git.js';
|
||||||
import { executeTask, type TaskExecutionOptions } from './taskExecution.js';
|
import { executeTask, type TaskExecutionOptions } from './taskExecution.js';
|
||||||
import { loadGlobalConfig } from '../config/globalConfig.js';
|
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||||
import { info, error, success, status } from '../utils/ui.js';
|
import { info, error, success, status, blankLine } from '../utils/ui.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
import type { PipelineConfig } from '../models/types.js';
|
import type { PipelineConfig } from '../models/types.js';
|
||||||
import {
|
import {
|
||||||
EXIT_ISSUE_FETCH_FAILED,
|
EXIT_ISSUE_FETCH_FAILED,
|
||||||
@ -74,29 +76,6 @@ function createBranch(cwd: string, branch: string): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stage all changes and create a commit */
|
|
||||||
function commitChanges(cwd: string, message: string): string | undefined {
|
|
||||||
execFileSync('git', ['add', '-A'], { cwd, stdio: 'pipe' });
|
|
||||||
|
|
||||||
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
|
||||||
cwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!statusOutput.trim()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
execFileSync('git', ['commit', '-m', message], { cwd, stdio: 'pipe' });
|
|
||||||
|
|
||||||
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
||||||
cwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
encoding: 'utf-8',
|
|
||||||
}).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build commit message from template or defaults */
|
/** Build commit message from template or defaults */
|
||||||
function buildCommitMessage(
|
function buildCommitMessage(
|
||||||
pipelineConfig: PipelineConfig | undefined,
|
pipelineConfig: PipelineConfig | undefined,
|
||||||
@ -159,7 +138,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
|||||||
task = formatIssueAsTask(issue);
|
task = formatIssueAsTask(issue);
|
||||||
success(`Issue #${options.issueNumber} fetched: "${issue.title}"`);
|
success(`Issue #${options.issueNumber} fetched: "${issue.title}"`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(`Failed to fetch issue #${options.issueNumber}: ${err instanceof Error ? err.message : String(err)}`);
|
error(`Failed to fetch issue #${options.issueNumber}: ${getErrorMessage(err)}`);
|
||||||
return EXIT_ISSUE_FETCH_FAILED;
|
return EXIT_ISSUE_FETCH_FAILED;
|
||||||
}
|
}
|
||||||
} else if (options.task) {
|
} else if (options.task) {
|
||||||
@ -178,7 +157,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
|||||||
createBranch(cwd, branch);
|
createBranch(cwd, branch);
|
||||||
success(`Branch created: ${branch}`);
|
success(`Branch created: ${branch}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(`Failed to create branch: ${err instanceof Error ? err.message : String(err)}`);
|
error(`Failed to create branch: ${getErrorMessage(err)}`);
|
||||||
return EXIT_GIT_OPERATION_FAILED;
|
return EXIT_GIT_OPERATION_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,7 +170,13 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
|||||||
? { provider: options.provider, model: options.model }
|
? { provider: options.provider, model: options.model }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const taskSuccess = await executeTask(task, cwd, workflow, cwd, agentOverrides);
|
const taskSuccess = await executeTask({
|
||||||
|
task,
|
||||||
|
cwd,
|
||||||
|
workflowIdentifier: workflow,
|
||||||
|
projectCwd: cwd,
|
||||||
|
agentOverrides,
|
||||||
|
});
|
||||||
|
|
||||||
if (!taskSuccess) {
|
if (!taskSuccess) {
|
||||||
error(`Workflow '${workflow}' failed`);
|
error(`Workflow '${workflow}' failed`);
|
||||||
@ -205,7 +190,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
|||||||
|
|
||||||
info('Committing changes...');
|
info('Committing changes...');
|
||||||
try {
|
try {
|
||||||
const commitHash = commitChanges(cwd, commitMessage);
|
const commitHash = stageAndCommit(cwd, commitMessage);
|
||||||
if (commitHash) {
|
if (commitHash) {
|
||||||
success(`Changes committed: ${commitHash}`);
|
success(`Changes committed: ${commitHash}`);
|
||||||
} else {
|
} else {
|
||||||
@ -216,7 +201,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
|||||||
pushBranch(cwd, branch);
|
pushBranch(cwd, branch);
|
||||||
success(`Pushed to origin/${branch}`);
|
success(`Pushed to origin/${branch}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(`Git operation failed: ${err instanceof Error ? err.message : String(err)}`);
|
error(`Git operation failed: ${getErrorMessage(err)}`);
|
||||||
return EXIT_GIT_OPERATION_FAILED;
|
return EXIT_GIT_OPERATION_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,7 +233,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Summary ---
|
// --- Summary ---
|
||||||
console.log();
|
blankLine();
|
||||||
status('Issue', issue ? `#${issue.number} "${issue.title}"` : 'N/A');
|
status('Issue', issue ? `#${issue.number} "${issue.title}"` : 'N/A');
|
||||||
status('Branch', branch ?? '(current)');
|
status('Branch', branch ?? '(current)');
|
||||||
status('Workflow', workflow);
|
status('Workflow', workflow);
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
error,
|
error,
|
||||||
success,
|
success,
|
||||||
status,
|
status,
|
||||||
|
blankLine,
|
||||||
} from '../utils/ui.js';
|
} from '../utils/ui.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
import { getErrorMessage } from '../utils/error.js';
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
@ -27,24 +28,25 @@ export interface TaskExecutionOptions {
|
|||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecuteTaskOptions {
|
||||||
|
/** Task content */
|
||||||
|
task: string;
|
||||||
|
/** Working directory (may be a clone path) */
|
||||||
|
cwd: string;
|
||||||
|
/** Workflow name or path (auto-detected by isWorkflowPath) */
|
||||||
|
workflowIdentifier: string;
|
||||||
|
/** Project root (where .takt/ lives) */
|
||||||
|
projectCwd: string;
|
||||||
|
/** Agent provider/model overrides */
|
||||||
|
agentOverrides?: TaskExecutionOptions;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a single task with workflow.
|
* Execute a single task with workflow.
|
||||||
*
|
|
||||||
* @param task - Task content
|
|
||||||
* @param cwd - Working directory (may be a clone path)
|
|
||||||
* @param workflowIdentifier - Workflow name or path (auto-detected by isWorkflowPath)
|
|
||||||
* @param projectCwd - Project root (where .takt/ lives). Defaults to cwd.
|
|
||||||
*/
|
*/
|
||||||
export async function executeTask(
|
export async function executeTask(options: ExecuteTaskOptions): Promise<boolean> {
|
||||||
task: string,
|
const { task, cwd, workflowIdentifier, projectCwd, agentOverrides } = options;
|
||||||
cwd: string,
|
const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, projectCwd);
|
||||||
workflowIdentifier: string = DEFAULT_WORKFLOW_NAME,
|
|
||||||
projectCwd?: string,
|
|
||||||
options?: TaskExecutionOptions
|
|
||||||
): Promise<boolean> {
|
|
||||||
const effectiveProjectCwd = projectCwd || cwd;
|
|
||||||
|
|
||||||
const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, effectiveProjectCwd);
|
|
||||||
|
|
||||||
if (!workflowConfig) {
|
if (!workflowConfig) {
|
||||||
if (isWorkflowPath(workflowIdentifier)) {
|
if (isWorkflowPath(workflowIdentifier)) {
|
||||||
@ -66,8 +68,8 @@ export async function executeTask(
|
|||||||
const result = await executeWorkflow(workflowConfig, task, cwd, {
|
const result = await executeWorkflow(workflowConfig, task, cwd, {
|
||||||
projectCwd,
|
projectCwd,
|
||||||
language: globalConfig.language,
|
language: globalConfig.language,
|
||||||
provider: options?.provider,
|
provider: agentOverrides?.provider,
|
||||||
model: options?.model,
|
model: agentOverrides?.model,
|
||||||
});
|
});
|
||||||
return result.success;
|
return result.success;
|
||||||
}
|
}
|
||||||
@ -94,7 +96,13 @@ export async function executeAndCompleteTask(
|
|||||||
const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName);
|
const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName);
|
||||||
|
|
||||||
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
||||||
const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, cwd, options);
|
const taskSuccess = await executeTask({
|
||||||
|
task: task.content,
|
||||||
|
cwd: execCwd,
|
||||||
|
workflowIdentifier: execWorkflow,
|
||||||
|
projectCwd: cwd,
|
||||||
|
agentOverrides: options,
|
||||||
|
});
|
||||||
const completedAt = new Date().toISOString();
|
const completedAt = new Date().toISOString();
|
||||||
|
|
||||||
if (taskSuccess && isWorktree) {
|
if (taskSuccess && isWorktree) {
|
||||||
@ -169,9 +177,9 @@ export async function runAllTasks(
|
|||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
|
||||||
while (task) {
|
while (task) {
|
||||||
console.log();
|
blankLine();
|
||||||
info(`=== Task: ${task.name} ===`);
|
info(`=== Task: ${task.name} ===`);
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options);
|
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options);
|
||||||
|
|
||||||
@ -186,7 +194,7 @@ export async function runAllTasks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalCount = successCount + failCount;
|
const totalCount = successCount + failCount;
|
||||||
console.log();
|
blankLine();
|
||||||
header('Tasks Summary');
|
header('Tasks Summary');
|
||||||
status('Total', String(totalCount));
|
status('Total', String(totalCount));
|
||||||
status('Success', String(successCount), successCount === totalCount ? 'green' : undefined);
|
status('Success', String(successCount), successCount === totalCount ? 'green' : undefined);
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
info,
|
info,
|
||||||
success,
|
success,
|
||||||
status,
|
status,
|
||||||
|
blankLine,
|
||||||
} from '../utils/ui.js';
|
} from '../utils/ui.js';
|
||||||
import { executeAndCompleteTask } from './taskExecution.js';
|
import { executeAndCompleteTask } from './taskExecution.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
|
||||||
@ -35,11 +36,11 @@ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): P
|
|||||||
info(`Workflow: ${workflowName}`);
|
info(`Workflow: ${workflowName}`);
|
||||||
info(`Watching: ${taskRunner.getTasksDir()}`);
|
info(`Watching: ${taskRunner.getTasksDir()}`);
|
||||||
info('Waiting for tasks... (Ctrl+C to stop)');
|
info('Waiting for tasks... (Ctrl+C to stop)');
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
// Graceful shutdown on SIGINT
|
// Graceful shutdown on SIGINT
|
||||||
const onSigInt = () => {
|
const onSigInt = () => {
|
||||||
console.log();
|
blankLine();
|
||||||
info('Stopping watch...');
|
info('Stopping watch...');
|
||||||
watcher.stop();
|
watcher.stop();
|
||||||
};
|
};
|
||||||
@ -48,9 +49,9 @@ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): P
|
|||||||
try {
|
try {
|
||||||
await watcher.watch(async (task: TaskInfo) => {
|
await watcher.watch(async (task: TaskInfo) => {
|
||||||
taskCount++;
|
taskCount++;
|
||||||
console.log();
|
blankLine();
|
||||||
info(`=== Task ${taskCount}: ${task.name} ===`);
|
info(`=== Task ${taskCount}: ${task.name} ===`);
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options);
|
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options);
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): P
|
|||||||
failCount++;
|
failCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
blankLine();
|
||||||
info('Waiting for tasks... (Ctrl+C to stop)');
|
info('Waiting for tasks... (Ctrl+C to stop)');
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -69,7 +70,7 @@ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): P
|
|||||||
|
|
||||||
// Summary on exit
|
// Summary on exit
|
||||||
if (taskCount > 0) {
|
if (taskCount > 0) {
|
||||||
console.log();
|
blankLine();
|
||||||
header('Watch Summary');
|
header('Watch Summary');
|
||||||
status('Total', String(taskCount));
|
status('Total', String(taskCount));
|
||||||
status('Success', String(successCount), successCount === taskCount ? 'green' : undefined);
|
status('Success', String(successCount), successCount === taskCount ? 'green' : undefined);
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
error,
|
error,
|
||||||
success,
|
success,
|
||||||
status,
|
status,
|
||||||
|
blankLine,
|
||||||
StreamDisplay,
|
StreamDisplay,
|
||||||
} from '../utils/ui.js';
|
} from '../utils/ui.js';
|
||||||
import {
|
import {
|
||||||
@ -71,8 +72,8 @@ export interface WorkflowExecutionResult {
|
|||||||
export interface WorkflowExecutionOptions {
|
export interface WorkflowExecutionOptions {
|
||||||
/** Header prefix for display */
|
/** Header prefix for display */
|
||||||
headerPrefix?: string;
|
headerPrefix?: string;
|
||||||
/** Project root directory (where .takt/ lives). Defaults to cwd. */
|
/** Project root directory (where .takt/ lives). */
|
||||||
projectCwd?: string;
|
projectCwd: string;
|
||||||
/** Language for instruction metadata */
|
/** Language for instruction metadata */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
@ -86,14 +87,14 @@ export async function executeWorkflow(
|
|||||||
workflowConfig: WorkflowConfig,
|
workflowConfig: WorkflowConfig,
|
||||||
task: string,
|
task: string,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
options: WorkflowExecutionOptions = {}
|
options: WorkflowExecutionOptions
|
||||||
): Promise<WorkflowExecutionResult> {
|
): Promise<WorkflowExecutionResult> {
|
||||||
const {
|
const {
|
||||||
headerPrefix = 'Running Workflow:',
|
headerPrefix = 'Running Workflow:',
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// projectCwd is where .takt/ lives (project root, not the clone)
|
// projectCwd is where .takt/ lives (project root, not the clone)
|
||||||
const projectCwd = options.projectCwd ?? cwd;
|
const projectCwd = options.projectCwd;
|
||||||
|
|
||||||
// Always continue from previous sessions (use /clear to reset)
|
// Always continue from previous sessions (use /clear to reset)
|
||||||
log.debug('Continuing session (use /clear to reset)');
|
log.debug('Continuing session (use /clear to reset)');
|
||||||
@ -144,7 +145,7 @@ export async function executeWorkflow(
|
|||||||
displayRef.current = null;
|
displayRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
blankLine();
|
||||||
warn(
|
warn(
|
||||||
`最大イテレーションに到達しました (${request.currentIteration}/${request.maxIterations})`
|
`最大イテレーションに到達しました (${request.currentIteration}/${request.maxIterations})`
|
||||||
);
|
);
|
||||||
@ -230,7 +231,7 @@ export async function executeWorkflow(
|
|||||||
displayRef.current.flush();
|
displayRef.current.flush();
|
||||||
displayRef.current = null;
|
displayRef.current = null;
|
||||||
}
|
}
|
||||||
console.log();
|
blankLine();
|
||||||
|
|
||||||
if (response.matchedRuleIndex != null && step.rules) {
|
if (response.matchedRuleIndex != null && step.rules) {
|
||||||
const rule = step.rules[response.matchedRuleIndex];
|
const rule = step.rules[response.matchedRuleIndex];
|
||||||
@ -334,11 +335,11 @@ export async function executeWorkflow(
|
|||||||
const onSigInt = () => {
|
const onSigInt = () => {
|
||||||
sigintCount++;
|
sigintCount++;
|
||||||
if (sigintCount === 1) {
|
if (sigintCount === 1) {
|
||||||
console.log();
|
blankLine();
|
||||||
warn('Ctrl+C: ワークフローを中断しています...');
|
warn('Ctrl+C: ワークフローを中断しています...');
|
||||||
engine.abort();
|
engine.abort();
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
blankLine();
|
||||||
error('Ctrl+C: 強制終了します');
|
error('Ctrl+C: 強制終了します');
|
||||||
process.exit(EXIT_SIGINT);
|
process.exit(EXIT_SIGINT);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,16 +23,29 @@ function createDefaultGlobalConfig(): GlobalConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Module-level cache for global configuration */
|
||||||
|
let cachedConfig: GlobalConfig | null = null;
|
||||||
|
|
||||||
|
/** Invalidate the cached global configuration (call after mutation) */
|
||||||
|
export function invalidateGlobalConfigCache(): void {
|
||||||
|
cachedConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Load global configuration */
|
/** Load global configuration */
|
||||||
export function loadGlobalConfig(): GlobalConfig {
|
export function loadGlobalConfig(): GlobalConfig {
|
||||||
|
if (cachedConfig !== null) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
const configPath = getGlobalConfigPath();
|
const configPath = getGlobalConfigPath();
|
||||||
if (!existsSync(configPath)) {
|
if (!existsSync(configPath)) {
|
||||||
return createDefaultGlobalConfig();
|
const defaultConfig = createDefaultGlobalConfig();
|
||||||
|
cachedConfig = defaultConfig;
|
||||||
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
const content = readFileSync(configPath, 'utf-8');
|
const content = readFileSync(configPath, 'utf-8');
|
||||||
const raw = parseYaml(content);
|
const raw = parseYaml(content);
|
||||||
const parsed = GlobalConfigSchema.parse(raw);
|
const parsed = GlobalConfigSchema.parse(raw);
|
||||||
return {
|
const config: GlobalConfig = {
|
||||||
language: parsed.language,
|
language: parsed.language,
|
||||||
trustedDirectories: parsed.trusted_directories,
|
trustedDirectories: parsed.trusted_directories,
|
||||||
defaultWorkflow: parsed.default_workflow,
|
defaultWorkflow: parsed.default_workflow,
|
||||||
@ -54,6 +67,8 @@ export function loadGlobalConfig(): GlobalConfig {
|
|||||||
} : undefined,
|
} : undefined,
|
||||||
minimalOutput: parsed.minimal_output,
|
minimalOutput: parsed.minimal_output,
|
||||||
};
|
};
|
||||||
|
cachedConfig = config;
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save global configuration */
|
/** Save global configuration */
|
||||||
@ -100,6 +115,7 @@ export function saveGlobalConfig(config: GlobalConfig): void {
|
|||||||
raw.minimal_output = config.minimalOutput;
|
raw.minimal_output = config.minimalOutput;
|
||||||
}
|
}
|
||||||
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get list of disabled builtin names */
|
/** Get list of disabled builtin names */
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
loadGlobalConfig,
|
loadGlobalConfig,
|
||||||
saveGlobalConfig,
|
saveGlobalConfig,
|
||||||
|
invalidateGlobalConfigCache,
|
||||||
addTrustedDirectory,
|
addTrustedDirectory,
|
||||||
isDirectoryTrusted,
|
isDirectoryTrusted,
|
||||||
loadProjectDebugConfig,
|
loadProjectDebugConfig,
|
||||||
|
|||||||
@ -28,8 +28,6 @@ export interface ProjectLocalConfig {
|
|||||||
provider?: 'claude' | 'codex';
|
provider?: 'claude' | 'codex';
|
||||||
/** Permission mode setting */
|
/** Permission mode setting */
|
||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
/** @deprecated Use permissionMode instead. Auto-approve all permissions in this project */
|
|
||||||
sacrificeMode?: boolean;
|
|
||||||
/** Verbose output mode */
|
/** Verbose output mode */
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
/** Custom settings */
|
/** Custom settings */
|
||||||
|
|||||||
@ -246,10 +246,10 @@ function loadWorkflowFromFile(filePath: string): WorkflowConfig {
|
|||||||
/**
|
/**
|
||||||
* Resolve a path that may be relative, absolute, or home-directory-relative.
|
* Resolve a path that may be relative, absolute, or home-directory-relative.
|
||||||
* @param pathInput Path to resolve
|
* @param pathInput Path to resolve
|
||||||
* @param basePath Base directory for relative paths (defaults to cwd)
|
* @param basePath Base directory for relative paths
|
||||||
* @returns Absolute resolved path
|
* @returns Absolute resolved path
|
||||||
*/
|
*/
|
||||||
function resolvePath(pathInput: string, basePath: string = process.cwd()): string {
|
function resolvePath(pathInput: string, basePath: string): string {
|
||||||
// Home directory expansion
|
// Home directory expansion
|
||||||
if (pathInput.startsWith('~')) {
|
if (pathInput.startsWith('~')) {
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
@ -270,12 +270,12 @@ function resolvePath(pathInput: string, basePath: string = process.cwd()): strin
|
|||||||
* Called internally by loadWorkflowByIdentifier when the identifier is detected as a path.
|
* Called internally by loadWorkflowByIdentifier when the identifier is detected as a path.
|
||||||
*
|
*
|
||||||
* @param filePath Path to workflow file (absolute, relative, or home-dir prefixed with ~)
|
* @param filePath Path to workflow file (absolute, relative, or home-dir prefixed with ~)
|
||||||
* @param basePath Base directory for resolving relative paths (default: cwd)
|
* @param basePath Base directory for resolving relative paths
|
||||||
* @returns WorkflowConfig or null if file not found
|
* @returns WorkflowConfig or null if file not found
|
||||||
*/
|
*/
|
||||||
function loadWorkflowFromPath(
|
function loadWorkflowFromPath(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
basePath: string = process.cwd()
|
basePath: string
|
||||||
): WorkflowConfig | null {
|
): WorkflowConfig | null {
|
||||||
const resolvedPath = resolvePath(filePath, basePath);
|
const resolvedPath = resolvePath(filePath, basePath);
|
||||||
|
|
||||||
@ -295,11 +295,11 @@ function loadWorkflowFromPath(
|
|||||||
* 3. Builtin workflows → resources/global/{lang}/workflows/{name}.yaml
|
* 3. Builtin workflows → resources/global/{lang}/workflows/{name}.yaml
|
||||||
*
|
*
|
||||||
* @param name Workflow name (not a file path)
|
* @param name Workflow name (not a file path)
|
||||||
* @param projectCwd Project root directory (default: cwd, for project-local workflow resolution)
|
* @param projectCwd Project root directory (for project-local workflow resolution)
|
||||||
*/
|
*/
|
||||||
export function loadWorkflow(
|
export function loadWorkflow(
|
||||||
name: string,
|
name: string,
|
||||||
projectCwd: string = process.cwd()
|
projectCwd: string
|
||||||
): WorkflowConfig | null {
|
): WorkflowConfig | null {
|
||||||
// 1. Project-local workflow (.takt/workflows/{name}.yaml)
|
// 1. Project-local workflow (.takt/workflows/{name}.yaml)
|
||||||
const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows');
|
const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows');
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
import { checkGhCli, type GitHubIssue } from './issue.js';
|
import { checkGhCli, type GitHubIssue } from './issue.js';
|
||||||
|
|
||||||
const log = createLogger('github-pr');
|
const log = createLogger('github-pr');
|
||||||
@ -81,7 +82,7 @@ export function createPullRequest(cwd: string, options: CreatePrOptions): Create
|
|||||||
|
|
||||||
return { success: true, url };
|
return { success: true, url };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = getErrorMessage(err);
|
||||||
log.error('PR creation failed', { error: errorMessage });
|
log.error('PR creation failed', { error: errorMessage });
|
||||||
return { success: false, error: errorMessage };
|
return { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
|
import { stageAndCommit } from './git.js';
|
||||||
|
|
||||||
const log = createLogger('autoCommit');
|
const log = createLogger('autoCommit');
|
||||||
|
|
||||||
@ -38,38 +40,14 @@ export function autoCommitAndPush(cloneCwd: string, taskName: string, projectDir
|
|||||||
log.info('Auto-commit starting', { cwd: cloneCwd, taskName });
|
log.info('Auto-commit starting', { cwd: cloneCwd, taskName });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Stage all changes
|
const commitMessage = `takt: ${taskName}`;
|
||||||
execFileSync('git', ['add', '-A'], {
|
const commitHash = stageAndCommit(cloneCwd, commitMessage);
|
||||||
cwd: cloneCwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if there are staged changes
|
if (!commitHash) {
|
||||||
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
|
||||||
cwd: cloneCwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!statusOutput.trim()) {
|
|
||||||
log.info('No changes to commit');
|
log.info('No changes to commit');
|
||||||
return { success: true, message: 'No changes to commit' };
|
return { success: true, message: 'No changes to commit' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create commit (no co-author)
|
|
||||||
const commitMessage = `takt: ${taskName}`;
|
|
||||||
execFileSync('git', ['commit', '-m', commitMessage], {
|
|
||||||
cwd: cloneCwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the short commit hash
|
|
||||||
const commitHash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
||||||
cwd: cloneCwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
encoding: 'utf-8',
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
log.info('Auto-commit created', { commitHash, message: commitMessage });
|
log.info('Auto-commit created', { commitHash, message: commitMessage });
|
||||||
|
|
||||||
// Push directly to the main repo (origin was removed to isolate the clone)
|
// Push directly to the main repo (origin was removed to isolate the clone)
|
||||||
@ -86,7 +64,7 @@ export function autoCommitAndPush(cloneCwd: string, taskName: string, projectDir
|
|||||||
message: `Committed & pushed: ${commitHash} - ${commitMessage}`,
|
message: `Committed & pushed: ${commitHash} - ${commitMessage}`,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = getErrorMessage(err);
|
||||||
log.error('Auto-commit failed', { error: errorMessage });
|
log.error('Auto-commit failed', { error: errorMessage });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
31
src/task/git.ts
Normal file
31
src/task/git.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Shared git operations for task execution
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage all changes and create a commit.
|
||||||
|
* Returns the short commit hash if changes were committed, undefined if no changes.
|
||||||
|
*/
|
||||||
|
export function stageAndCommit(cwd: string, message: string): string | undefined {
|
||||||
|
execFileSync('git', ['add', '-A'], { cwd, stdio: 'pipe' });
|
||||||
|
|
||||||
|
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
||||||
|
cwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!statusOutput.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
execFileSync('git', ['commit', '-m', message], { cwd, stdio: 'pipe' });
|
||||||
|
|
||||||
|
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||||
|
cwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
@ -29,6 +29,11 @@ function shouldLog(level: LogLevel): boolean {
|
|||||||
return LOG_PRIORITIES[level] >= LOG_PRIORITIES[currentLogLevel];
|
return LOG_PRIORITIES[level] >= LOG_PRIORITIES[currentLogLevel];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Print a blank line */
|
||||||
|
export function blankLine(): void {
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
/** Log a debug message */
|
/** Log a debug message */
|
||||||
export function debug(message: string): void {
|
export function debug(message: string): void {
|
||||||
if (shouldLog('debug')) {
|
if (shouldLog('debug')) {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
incrementStepIteration,
|
incrementStepIteration,
|
||||||
} from './state-manager.js';
|
} from './state-manager.js';
|
||||||
import { generateReportDir } from '../utils/session.js';
|
import { generateReportDir } from '../utils/session.js';
|
||||||
|
import { getErrorMessage } from '../utils/error.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../utils/debug.js';
|
||||||
import { interruptAllQueries } from '../claude/query-manager.js';
|
import { interruptAllQueries } from '../claude/query-manager.js';
|
||||||
|
|
||||||
@ -57,10 +58,10 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
private reportDir: string;
|
private reportDir: string;
|
||||||
private abortRequested = false;
|
private abortRequested = false;
|
||||||
|
|
||||||
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) {
|
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions) {
|
||||||
super();
|
super();
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.projectCwd = options.projectCwd ?? cwd;
|
this.projectCwd = options.projectCwd;
|
||||||
this.cwd = cwd;
|
this.cwd = cwd;
|
||||||
this.task = task;
|
this.task = task;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@ -553,7 +554,7 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
if (this.abortRequested) {
|
if (this.abortRequested) {
|
||||||
this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)');
|
this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)');
|
||||||
} else {
|
} else {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = getErrorMessage(error);
|
||||||
this.emit('workflow:abort', this.state, ERROR_MESSAGES.STEP_EXECUTION_FAILED(message));
|
this.emit('workflow:abort', this.state, ERROR_MESSAGES.STEP_EXECUTION_FAILED(message));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -11,196 +11,16 @@
|
|||||||
* and also used in Phase 3 (buildStatusJudgmentInstruction) as a dedicated follow-up.
|
* and also used in Phase 3 (buildStatusJudgmentInstruction) as a dedicated follow-up.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep, WorkflowRule, AgentResponse, Language, ReportConfig, ReportObjectConfig } from '../models/types.js';
|
import type { WorkflowStep, Language, ReportConfig, ReportObjectConfig } from '../models/types.js';
|
||||||
import { hasTagBasedRules } from './rule-utils.js';
|
import { hasTagBasedRules } from './rule-utils.js';
|
||||||
|
import type { InstructionContext } from './instruction-context.js';
|
||||||
|
import { buildExecutionMetadata, renderExecutionMetadata, METADATA_STRINGS } from './instruction-context.js';
|
||||||
|
import { generateStatusRulesFromRules } from './status-rules.js';
|
||||||
|
|
||||||
|
// Re-export from sub-modules for backward compatibility
|
||||||
/**
|
export type { InstructionContext, ExecutionMetadata } from './instruction-context.js';
|
||||||
* Context for building instruction from template.
|
export { buildExecutionMetadata, renderExecutionMetadata } from './instruction-context.js';
|
||||||
*/
|
export { generateStatusRulesFromRules } from './status-rules.js';
|
||||||
export interface InstructionContext {
|
|
||||||
/** The main task/prompt */
|
|
||||||
task: string;
|
|
||||||
/** Current iteration number (workflow-wide turn count) */
|
|
||||||
iteration: number;
|
|
||||||
/** Maximum iterations allowed */
|
|
||||||
maxIterations: number;
|
|
||||||
/** Current step's iteration number (how many times this step has been executed) */
|
|
||||||
stepIteration: number;
|
|
||||||
/** Working directory (agent work dir, may be a clone) */
|
|
||||||
cwd: string;
|
|
||||||
/** Project root directory (where .takt/ lives). Defaults to cwd. */
|
|
||||||
projectCwd?: string;
|
|
||||||
/** User inputs accumulated during workflow */
|
|
||||||
userInputs: string[];
|
|
||||||
/** Previous step output if available */
|
|
||||||
previousOutput?: AgentResponse;
|
|
||||||
/** Report directory path */
|
|
||||||
reportDir?: string;
|
|
||||||
/** Language for metadata rendering. Defaults to 'en'. */
|
|
||||||
language?: Language;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Execution environment metadata prepended to agent instructions */
|
|
||||||
export interface ExecutionMetadata {
|
|
||||||
/** The agent's working directory (may be a clone) */
|
|
||||||
readonly workingDirectory: string;
|
|
||||||
/** Language for metadata rendering */
|
|
||||||
readonly language: Language;
|
|
||||||
/** Whether file editing is allowed for this step (undefined = no prompt) */
|
|
||||||
readonly edit?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build execution metadata from instruction context and step config.
|
|
||||||
*
|
|
||||||
* Pure function: (InstructionContext, edit?) → ExecutionMetadata.
|
|
||||||
*/
|
|
||||||
export function buildExecutionMetadata(context: InstructionContext, edit?: boolean): ExecutionMetadata {
|
|
||||||
return {
|
|
||||||
workingDirectory: context.cwd,
|
|
||||||
language: context.language ?? 'en',
|
|
||||||
edit,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Localized strings for rules-based status prompt */
|
|
||||||
const RULES_PROMPT_STRINGS = {
|
|
||||||
en: {
|
|
||||||
criteriaHeading: '## Decision Criteria',
|
|
||||||
headerNum: '#',
|
|
||||||
headerCondition: 'Condition',
|
|
||||||
headerTag: 'Tag',
|
|
||||||
outputHeading: '## Output Format',
|
|
||||||
outputInstruction: 'Output the tag corresponding to your decision:',
|
|
||||||
appendixHeading: '### Appendix Template',
|
|
||||||
appendixInstruction: 'When outputting `[{tag}]`, append the following:',
|
|
||||||
},
|
|
||||||
ja: {
|
|
||||||
criteriaHeading: '## 判定基準',
|
|
||||||
headerNum: '#',
|
|
||||||
headerCondition: '状況',
|
|
||||||
headerTag: 'タグ',
|
|
||||||
outputHeading: '## 出力フォーマット',
|
|
||||||
outputInstruction: '判定に対応するタグを出力してください:',
|
|
||||||
appendixHeading: '### 追加出力テンプレート',
|
|
||||||
appendixInstruction: '`[{tag}]` を出力する場合、以下を追記してください:',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate status rules prompt from rules configuration.
|
|
||||||
* Creates a structured prompt that tells the agent which numbered tags to output.
|
|
||||||
*
|
|
||||||
* Example output for step "plan" with 3 rules:
|
|
||||||
* ## 判定基準
|
|
||||||
* | # | 状況 | タグ |
|
|
||||||
* |---|------|------|
|
|
||||||
* | 1 | 要件が明確で実装可能 | `[PLAN:1]` |
|
|
||||||
* | 2 | ユーザーが質問をしている | `[PLAN:2]` |
|
|
||||||
* | 3 | 要件が不明確、情報不足 | `[PLAN:3]` |
|
|
||||||
*/
|
|
||||||
export function generateStatusRulesFromRules(
|
|
||||||
stepName: string,
|
|
||||||
rules: WorkflowRule[],
|
|
||||||
language: Language,
|
|
||||||
): string {
|
|
||||||
const tag = stepName.toUpperCase();
|
|
||||||
const strings = RULES_PROMPT_STRINGS[language];
|
|
||||||
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
// Criteria table
|
|
||||||
lines.push(strings.criteriaHeading);
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`);
|
|
||||||
lines.push('|---|------|------|');
|
|
||||||
for (const [i, rule] of rules.entries()) {
|
|
||||||
lines.push(`| ${i + 1} | ${rule.condition} | \`[${tag}:${i + 1}]\` |`);
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
|
|
||||||
// Output format
|
|
||||||
lines.push(strings.outputHeading);
|
|
||||||
lines.push('');
|
|
||||||
lines.push(strings.outputInstruction);
|
|
||||||
lines.push('');
|
|
||||||
for (const [i, rule] of rules.entries()) {
|
|
||||||
lines.push(`- \`[${tag}:${i + 1}]\` — ${rule.condition}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appendix templates (if any rules have appendix)
|
|
||||||
const rulesWithAppendix = rules.filter((r) => r.appendix);
|
|
||||||
if (rulesWithAppendix.length > 0) {
|
|
||||||
lines.push('');
|
|
||||||
lines.push(strings.appendixHeading);
|
|
||||||
for (const [i, rule] of rules.entries()) {
|
|
||||||
if (!rule.appendix) continue;
|
|
||||||
const tagStr = `[${tag}:${i + 1}]`;
|
|
||||||
lines.push('');
|
|
||||||
lines.push(strings.appendixInstruction.replace('{tag}', tagStr));
|
|
||||||
lines.push('```');
|
|
||||||
lines.push(rule.appendix.trimEnd());
|
|
||||||
lines.push('```');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Localized strings for execution metadata rendering */
|
|
||||||
const METADATA_STRINGS = {
|
|
||||||
en: {
|
|
||||||
heading: '## Execution Context',
|
|
||||||
workingDirectory: 'Working Directory',
|
|
||||||
rulesHeading: '## Execution Rules',
|
|
||||||
noCommit: '**Do NOT run git commit.** Commits are handled automatically by the system after workflow completion.',
|
|
||||||
noCd: '**Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories.',
|
|
||||||
editEnabled: '**Editing is ENABLED for this step.** You may create, modify, and delete files as needed to fulfill the user\'s request.',
|
|
||||||
editDisabled: '**Editing is DISABLED for this step.** Do NOT create, modify, or delete any project source files. You may only read/search code and write to report files in the Report Directory.',
|
|
||||||
note: 'Note: This section is metadata. Follow the language used in the rest of the prompt.',
|
|
||||||
},
|
|
||||||
ja: {
|
|
||||||
heading: '## 実行コンテキスト',
|
|
||||||
workingDirectory: '作業ディレクトリ',
|
|
||||||
rulesHeading: '## 実行ルール',
|
|
||||||
noCommit: '**git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。',
|
|
||||||
noCd: '**Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。',
|
|
||||||
editEnabled: '**このステップでは編集が許可されています。** ユーザーの要求に応じて、ファイルの作成・変更・削除を行ってください。',
|
|
||||||
editDisabled: '**このステップでは編集が禁止されています。** プロジェクトのソースファイルを作成・変更・削除しないでください。コードの読み取り・検索と、Report Directoryへのレポート出力のみ行えます。',
|
|
||||||
note: '',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render execution metadata as a markdown string.
|
|
||||||
*
|
|
||||||
* Pure function: ExecutionMetadata → string.
|
|
||||||
* Always includes heading + Working Directory + Execution Rules.
|
|
||||||
* Language determines the output language; 'en' includes a note about language consistency.
|
|
||||||
*/
|
|
||||||
export function renderExecutionMetadata(metadata: ExecutionMetadata): string {
|
|
||||||
const strings = METADATA_STRINGS[metadata.language];
|
|
||||||
const lines = [
|
|
||||||
strings.heading,
|
|
||||||
`- ${strings.workingDirectory}: ${metadata.workingDirectory}`,
|
|
||||||
'',
|
|
||||||
strings.rulesHeading,
|
|
||||||
`- ${strings.noCommit}`,
|
|
||||||
`- ${strings.noCd}`,
|
|
||||||
];
|
|
||||||
if (metadata.edit === true) {
|
|
||||||
lines.push(`- ${strings.editEnabled}`);
|
|
||||||
} else if (metadata.edit === false) {
|
|
||||||
lines.push(`- ${strings.editDisabled}`);
|
|
||||||
}
|
|
||||||
if (strings.note) {
|
|
||||||
lines.push('');
|
|
||||||
lines.push(strings.note);
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape special characters in dynamic content to prevent template injection.
|
* Escape special characters in dynamic content to prevent template injection.
|
||||||
@ -562,6 +382,7 @@ export function buildReportInstruction(
|
|||||||
maxIterations: 0,
|
maxIterations: 0,
|
||||||
stepIteration: context.stepIteration,
|
stepIteration: context.stepIteration,
|
||||||
cwd: context.cwd,
|
cwd: context.cwd,
|
||||||
|
projectCwd: context.cwd,
|
||||||
userInputs: [],
|
userInputs: [],
|
||||||
reportDir: context.reportDir,
|
reportDir: context.reportDir,
|
||||||
language,
|
language,
|
||||||
|
|||||||
111
src/workflow/instruction-context.ts
Normal file
111
src/workflow/instruction-context.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Instruction context types and execution metadata rendering
|
||||||
|
*
|
||||||
|
* Defines the context structures used by instruction builders,
|
||||||
|
* and renders execution metadata (working directory, rules) as markdown.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AgentResponse, Language } from '../models/types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context for building instruction from template.
|
||||||
|
*/
|
||||||
|
export interface InstructionContext {
|
||||||
|
/** The main task/prompt */
|
||||||
|
task: string;
|
||||||
|
/** Current iteration number (workflow-wide turn count) */
|
||||||
|
iteration: number;
|
||||||
|
/** Maximum iterations allowed */
|
||||||
|
maxIterations: number;
|
||||||
|
/** Current step's iteration number (how many times this step has been executed) */
|
||||||
|
stepIteration: number;
|
||||||
|
/** Working directory (agent work dir, may be a clone) */
|
||||||
|
cwd: string;
|
||||||
|
/** Project root directory (where .takt/ lives). */
|
||||||
|
projectCwd: string;
|
||||||
|
/** User inputs accumulated during workflow */
|
||||||
|
userInputs: string[];
|
||||||
|
/** Previous step output if available */
|
||||||
|
previousOutput?: AgentResponse;
|
||||||
|
/** Report directory path */
|
||||||
|
reportDir?: string;
|
||||||
|
/** Language for metadata rendering. Defaults to 'en'. */
|
||||||
|
language?: Language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execution environment metadata prepended to agent instructions */
|
||||||
|
export interface ExecutionMetadata {
|
||||||
|
/** The agent's working directory (may be a clone) */
|
||||||
|
readonly workingDirectory: string;
|
||||||
|
/** Language for metadata rendering */
|
||||||
|
readonly language: Language;
|
||||||
|
/** Whether file editing is allowed for this step (undefined = no prompt) */
|
||||||
|
readonly edit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build execution metadata from instruction context and step config.
|
||||||
|
*
|
||||||
|
* Pure function: (InstructionContext, edit?) → ExecutionMetadata.
|
||||||
|
*/
|
||||||
|
export function buildExecutionMetadata(context: InstructionContext, edit?: boolean): ExecutionMetadata {
|
||||||
|
return {
|
||||||
|
workingDirectory: context.cwd,
|
||||||
|
language: context.language ?? 'en',
|
||||||
|
edit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Localized strings for execution metadata rendering */
|
||||||
|
export const METADATA_STRINGS = {
|
||||||
|
en: {
|
||||||
|
heading: '## Execution Context',
|
||||||
|
workingDirectory: 'Working Directory',
|
||||||
|
rulesHeading: '## Execution Rules',
|
||||||
|
noCommit: '**Do NOT run git commit.** Commits are handled automatically by the system after workflow completion.',
|
||||||
|
noCd: '**Do NOT use `cd` in Bash commands.** Your working directory is already set correctly. Run commands directly without changing directories.',
|
||||||
|
editEnabled: '**Editing is ENABLED for this step.** You may create, modify, and delete files as needed to fulfill the user\'s request.',
|
||||||
|
editDisabled: '**Editing is DISABLED for this step.** Do NOT create, modify, or delete any project source files. You may only read/search code and write to report files in the Report Directory.',
|
||||||
|
note: 'Note: This section is metadata. Follow the language used in the rest of the prompt.',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
heading: '## 実行コンテキスト',
|
||||||
|
workingDirectory: '作業ディレクトリ',
|
||||||
|
rulesHeading: '## 実行ルール',
|
||||||
|
noCommit: '**git commit を実行しないでください。** コミットはワークフロー完了後にシステムが自動で行います。',
|
||||||
|
noCd: '**Bashコマンドで `cd` を使用しないでください。** 作業ディレクトリは既に正しく設定されています。ディレクトリを変更せずにコマンドを実行してください。',
|
||||||
|
editEnabled: '**このステップでは編集が許可されています。** ユーザーの要求に応じて、ファイルの作成・変更・削除を行ってください。',
|
||||||
|
editDisabled: '**このステップでは編集が禁止されています。** プロジェクトのソースファイルを作成・変更・削除しないでください。コードの読み取り・検索と、Report Directoryへのレポート出力のみ行えます。',
|
||||||
|
note: '',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render execution metadata as a markdown string.
|
||||||
|
*
|
||||||
|
* Pure function: ExecutionMetadata → string.
|
||||||
|
* Always includes heading + Working Directory + Execution Rules.
|
||||||
|
* Language determines the output language; 'en' includes a note about language consistency.
|
||||||
|
*/
|
||||||
|
export function renderExecutionMetadata(metadata: ExecutionMetadata): string {
|
||||||
|
const strings = METADATA_STRINGS[metadata.language];
|
||||||
|
const lines = [
|
||||||
|
strings.heading,
|
||||||
|
`- ${strings.workingDirectory}: ${metadata.workingDirectory}`,
|
||||||
|
'',
|
||||||
|
strings.rulesHeading,
|
||||||
|
`- ${strings.noCommit}`,
|
||||||
|
`- ${strings.noCd}`,
|
||||||
|
];
|
||||||
|
if (metadata.edit === true) {
|
||||||
|
lines.push(`- ${strings.editEnabled}`);
|
||||||
|
} else if (metadata.edit === false) {
|
||||||
|
lines.push(`- ${strings.editDisabled}`);
|
||||||
|
}
|
||||||
|
if (strings.note) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push(strings.note);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
92
src/workflow/status-rules.ts
Normal file
92
src/workflow/status-rules.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Status rules prompt generation for workflow steps
|
||||||
|
*
|
||||||
|
* Generates structured prompts that tell agents which numbered tags to output
|
||||||
|
* based on the step's rule configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { WorkflowRule, Language } from '../models/types.js';
|
||||||
|
|
||||||
|
/** Localized strings for rules-based status prompt */
|
||||||
|
const RULES_PROMPT_STRINGS = {
|
||||||
|
en: {
|
||||||
|
criteriaHeading: '## Decision Criteria',
|
||||||
|
headerNum: '#',
|
||||||
|
headerCondition: 'Condition',
|
||||||
|
headerTag: 'Tag',
|
||||||
|
outputHeading: '## Output Format',
|
||||||
|
outputInstruction: 'Output the tag corresponding to your decision:',
|
||||||
|
appendixHeading: '### Appendix Template',
|
||||||
|
appendixInstruction: 'When outputting `[{tag}]`, append the following:',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
criteriaHeading: '## 判定基準',
|
||||||
|
headerNum: '#',
|
||||||
|
headerCondition: '状況',
|
||||||
|
headerTag: 'タグ',
|
||||||
|
outputHeading: '## 出力フォーマット',
|
||||||
|
outputInstruction: '判定に対応するタグを出力してください:',
|
||||||
|
appendixHeading: '### 追加出力テンプレート',
|
||||||
|
appendixInstruction: '`[{tag}]` を出力する場合、以下を追記してください:',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate status rules prompt from rules configuration.
|
||||||
|
* Creates a structured prompt that tells the agent which numbered tags to output.
|
||||||
|
*
|
||||||
|
* Example output for step "plan" with 3 rules:
|
||||||
|
* ## 判定基準
|
||||||
|
* | # | 状況 | タグ |
|
||||||
|
* |---|------|------|
|
||||||
|
* | 1 | 要件が明確で実装可能 | `[PLAN:1]` |
|
||||||
|
* | 2 | ユーザーが質問をしている | `[PLAN:2]` |
|
||||||
|
* | 3 | 要件が不明確、情報不足 | `[PLAN:3]` |
|
||||||
|
*/
|
||||||
|
export function generateStatusRulesFromRules(
|
||||||
|
stepName: string,
|
||||||
|
rules: WorkflowRule[],
|
||||||
|
language: Language,
|
||||||
|
): string {
|
||||||
|
const tag = stepName.toUpperCase();
|
||||||
|
const strings = RULES_PROMPT_STRINGS[language];
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// Criteria table
|
||||||
|
lines.push(strings.criteriaHeading);
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`);
|
||||||
|
lines.push('|---|------|------|');
|
||||||
|
for (const [i, rule] of rules.entries()) {
|
||||||
|
lines.push(`| ${i + 1} | ${rule.condition} | \`[${tag}:${i + 1}]\` |`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
// Output format
|
||||||
|
lines.push(strings.outputHeading);
|
||||||
|
lines.push('');
|
||||||
|
lines.push(strings.outputInstruction);
|
||||||
|
lines.push('');
|
||||||
|
for (const [i, rule] of rules.entries()) {
|
||||||
|
lines.push(`- \`[${tag}:${i + 1}]\` — ${rule.condition}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appendix templates (if any rules have appendix)
|
||||||
|
const rulesWithAppendix = rules.filter((r) => r.appendix);
|
||||||
|
if (rulesWithAppendix.length > 0) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push(strings.appendixHeading);
|
||||||
|
for (const [i, rule] of rules.entries()) {
|
||||||
|
if (!rule.appendix) continue;
|
||||||
|
const tagStr = `[${tag}:${i + 1}]`;
|
||||||
|
lines.push('');
|
||||||
|
lines.push(strings.appendixInstruction.replace('{tag}', tagStr));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(rule.appendix.trimEnd());
|
||||||
|
lines.push('```');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
@ -72,8 +72,8 @@ export interface WorkflowEngineOptions {
|
|||||||
onIterationLimit?: IterationLimitCallback;
|
onIterationLimit?: IterationLimitCallback;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
||||||
bypassPermissions?: boolean;
|
bypassPermissions?: boolean;
|
||||||
/** Project root directory (where .takt/ lives). Defaults to cwd if not specified. */
|
/** Project root directory (where .takt/ lives). */
|
||||||
projectCwd?: string;
|
projectCwd: string;
|
||||||
/** Language for instruction metadata. Defaults to 'en'. */
|
/** Language for instruction metadata. Defaults to 'en'. */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user