refactor: 設定解決をloadConfigへ統一し不要設定を削除
This commit is contained in:
parent
faf6ebf063
commit
5dc79946f2
@ -76,7 +76,7 @@ vi.mock('../infra/task/index.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
||||||
loadGlobalConfig: vi.fn(() => ({ interactivePreviewMovements: 3 })),
|
loadConfig: vi.fn(() => ({ global: { interactivePreviewMovements: 3 }, project: {} })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/constants.js', () => ({
|
vi.mock('../shared/constants.js', () => ({
|
||||||
@ -107,7 +107,7 @@ vi.mock('../app/cli/helpers.js', () => ({
|
|||||||
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
||||||
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
||||||
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
|
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
|
||||||
import { loadGlobalConfig } from '../infra/config/index.js';
|
import { loadConfig } from '../infra/config/index.js';
|
||||||
import { confirm } from '../shared/prompt/index.js';
|
import { confirm } from '../shared/prompt/index.js';
|
||||||
import { isDirectTask } from '../app/cli/helpers.js';
|
import { isDirectTask } from '../app/cli/helpers.js';
|
||||||
import { executeDefaultAction } from '../app/cli/routing.js';
|
import { executeDefaultAction } from '../app/cli/routing.js';
|
||||||
@ -123,7 +123,7 @@ const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
|
|||||||
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
||||||
const mockInteractiveMode = vi.mocked(interactiveMode);
|
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||||
const mockSelectRecentSession = vi.mocked(selectRecentSession);
|
const mockSelectRecentSession = vi.mocked(selectRecentSession);
|
||||||
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
const mockLoadConfig = vi.mocked(loadConfig);
|
||||||
const mockConfirm = vi.mocked(confirm);
|
const mockConfirm = vi.mocked(confirm);
|
||||||
const mockIsDirectTask = vi.mocked(isDirectTask);
|
const mockIsDirectTask = vi.mocked(isDirectTask);
|
||||||
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
||||||
@ -483,7 +483,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
describe('session selection with provider=claude', () => {
|
describe('session selection with provider=claude', () => {
|
||||||
it('should pass selected session ID to interactiveMode when provider is claude', async () => {
|
it('should pass selected session ID to interactiveMode when provider is claude', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockLoadGlobalConfig.mockReturnValue({ interactivePreviewMovements: 3, provider: 'claude' });
|
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'claude' }, project: {} });
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
mockSelectRecentSession.mockResolvedValue('session-xyz');
|
mockSelectRecentSession.mockResolvedValue('session-xyz');
|
||||||
|
|
||||||
@ -506,7 +506,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
|
|
||||||
it('should not call selectRecentSession when user selects no in confirmation', async () => {
|
it('should not call selectRecentSession when user selects no in confirmation', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockLoadGlobalConfig.mockReturnValue({ interactivePreviewMovements: 3, provider: 'claude' });
|
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'claude' }, project: {} });
|
||||||
mockConfirm.mockResolvedValue(false);
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@ -525,7 +525,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
|
|
||||||
it('should not call selectRecentSession when provider is not claude', async () => {
|
it('should not call selectRecentSession when provider is not claude', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockLoadGlobalConfig.mockReturnValue({ interactivePreviewMovements: 3, provider: 'openai' });
|
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'openai' }, project: {} });
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|||||||
53
src/__tests__/config-env-overrides.test.ts
Normal file
53
src/__tests__/config-env-overrides.test.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
applyGlobalConfigEnvOverrides,
|
||||||
|
applyProjectConfigEnvOverrides,
|
||||||
|
envVarNameFromPath,
|
||||||
|
} from '../infra/config/env/config-env-overrides.js';
|
||||||
|
|
||||||
|
describe('config env overrides', () => {
|
||||||
|
const envBackup = { ...process.env };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const key of Object.keys(process.env)) {
|
||||||
|
if (!(key in envBackup)) {
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(envBackup)) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert dotted and camelCase paths to TAKT env variable names', () => {
|
||||||
|
expect(envVarNameFromPath('verbose')).toBe('TAKT_VERBOSE');
|
||||||
|
expect(envVarNameFromPath('provider_options.claude.sandbox.allow_unsandboxed_commands'))
|
||||||
|
.toBe('TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply global env overrides from generated env names', () => {
|
||||||
|
process.env.TAKT_LOG_LEVEL = 'debug';
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS = 'true';
|
||||||
|
|
||||||
|
const raw: Record<string, unknown> = {};
|
||||||
|
applyGlobalConfigEnvOverrides(raw);
|
||||||
|
|
||||||
|
expect(raw.log_level).toBe('debug');
|
||||||
|
expect(raw.provider_options).toEqual({
|
||||||
|
claude: {
|
||||||
|
sandbox: {
|
||||||
|
allow_unsandboxed_commands: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply project env overrides from generated env names', () => {
|
||||||
|
process.env.TAKT_VERBOSE = 'true';
|
||||||
|
|
||||||
|
const raw: Record<string, unknown> = {};
|
||||||
|
applyProjectConfigEnvOverrides(raw);
|
||||||
|
|
||||||
|
expect(raw.verbose).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -35,6 +35,8 @@ import {
|
|||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
getLanguage,
|
getLanguage,
|
||||||
loadProjectConfig,
|
loadProjectConfig,
|
||||||
|
isVerboseMode,
|
||||||
|
invalidateGlobalConfigCache,
|
||||||
} from '../infra/config/index.js';
|
} from '../infra/config/index.js';
|
||||||
|
|
||||||
describe('getBuiltinPiece', () => {
|
describe('getBuiltinPiece', () => {
|
||||||
@ -377,6 +379,154 @@ describe('setCurrentPiece', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadProjectConfig provider_options', () => {
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = join(tmpdir(), `takt-test-${randomUUID()}`);
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize provider_options into providerOptions (camelCase)', () => {
|
||||||
|
const projectConfigDir = getProjectConfigDir(testDir);
|
||||||
|
mkdirSync(projectConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(projectConfigDir, 'config.yaml'), [
|
||||||
|
'piece: default',
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: true',
|
||||||
|
' claude:',
|
||||||
|
' sandbox:',
|
||||||
|
' allow_unsandboxed_commands: true',
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
const config = loadProjectConfig(testDir);
|
||||||
|
|
||||||
|
expect(config.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply TAKT_PROVIDER_OPTIONS_* env overrides for project config', () => {
|
||||||
|
const original = process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS = 'false';
|
||||||
|
|
||||||
|
const config = loadProjectConfig(testDir);
|
||||||
|
expect(config.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS = original;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isVerboseMode', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let originalTaktConfigDir: string | undefined;
|
||||||
|
let originalTaktVerbose: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = join(tmpdir(), `takt-test-${randomUUID()}`);
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
originalTaktConfigDir = process.env.TAKT_CONFIG_DIR;
|
||||||
|
originalTaktVerbose = process.env.TAKT_VERBOSE;
|
||||||
|
process.env.TAKT_CONFIG_DIR = join(testDir, 'global-takt');
|
||||||
|
delete process.env.TAKT_VERBOSE;
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalTaktConfigDir === undefined) {
|
||||||
|
delete process.env.TAKT_CONFIG_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_CONFIG_DIR = originalTaktConfigDir;
|
||||||
|
}
|
||||||
|
if (originalTaktVerbose === undefined) {
|
||||||
|
delete process.env.TAKT_VERBOSE;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_VERBOSE = originalTaktVerbose;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return project verbose when project config has verbose: true', () => {
|
||||||
|
const projectConfigDir = getProjectConfigDir(testDir);
|
||||||
|
mkdirSync(projectConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(projectConfigDir, 'config.yaml'), 'piece: default\nverbose: true\n');
|
||||||
|
|
||||||
|
const globalConfigDir = process.env.TAKT_CONFIG_DIR!;
|
||||||
|
mkdirSync(globalConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(globalConfigDir, 'config.yaml'), 'verbose: false\n');
|
||||||
|
|
||||||
|
expect(isVerboseMode(testDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return project verbose when project config has verbose: false', () => {
|
||||||
|
const projectConfigDir = getProjectConfigDir(testDir);
|
||||||
|
mkdirSync(projectConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(projectConfigDir, 'config.yaml'), 'piece: default\nverbose: false\n');
|
||||||
|
|
||||||
|
const globalConfigDir = process.env.TAKT_CONFIG_DIR!;
|
||||||
|
mkdirSync(globalConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(globalConfigDir, 'config.yaml'), 'verbose: true\n');
|
||||||
|
|
||||||
|
expect(isVerboseMode(testDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to global verbose when project verbose is not set', () => {
|
||||||
|
const projectConfigDir = getProjectConfigDir(testDir);
|
||||||
|
mkdirSync(projectConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(projectConfigDir, 'config.yaml'), 'piece: default\n');
|
||||||
|
|
||||||
|
const globalConfigDir = process.env.TAKT_CONFIG_DIR!;
|
||||||
|
mkdirSync(globalConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(globalConfigDir, 'config.yaml'), 'verbose: true\n');
|
||||||
|
|
||||||
|
expect(isVerboseMode(testDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when neither project nor global verbose is set', () => {
|
||||||
|
expect(isVerboseMode(testDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize TAKT_VERBOSE over project and global config', () => {
|
||||||
|
const projectConfigDir = getProjectConfigDir(testDir);
|
||||||
|
mkdirSync(projectConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(projectConfigDir, 'config.yaml'), 'piece: default\nverbose: false\n');
|
||||||
|
|
||||||
|
const globalConfigDir = process.env.TAKT_CONFIG_DIR!;
|
||||||
|
mkdirSync(globalConfigDir, { recursive: true });
|
||||||
|
writeFileSync(join(globalConfigDir, 'config.yaml'), 'verbose: false\n');
|
||||||
|
|
||||||
|
process.env.TAKT_VERBOSE = 'true';
|
||||||
|
expect(isVerboseMode(testDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on TAKT_VERBOSE=0', () => {
|
||||||
|
process.env.TAKT_VERBOSE = '0';
|
||||||
|
expect(() => isVerboseMode(testDir)).toThrow('TAKT_VERBOSE must be one of: true, false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on invalid TAKT_VERBOSE value', () => {
|
||||||
|
process.env.TAKT_VERBOSE = 'yes';
|
||||||
|
expect(() => isVerboseMode(testDir)).toThrow('TAKT_VERBOSE must be one of: true, false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('loadInputHistory', () => {
|
describe('loadInputHistory', () => {
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
|
|
||||||
|
|||||||
138
src/__tests__/engine-provider-options.test.ts
Normal file
138
src/__tests__/engine-provider-options.test.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { rmSync } from 'node:fs';
|
||||||
|
|
||||||
|
vi.mock('../agents/runner.js', () => ({
|
||||||
|
runAgent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../core/piece/evaluation/index.js', () => ({
|
||||||
|
detectMatchedRule: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../core/piece/phase-runner.js', () => ({
|
||||||
|
needsStatusJudgmentPhase: vi.fn(),
|
||||||
|
runReportPhase: vi.fn(),
|
||||||
|
runStatusJudgmentPhase: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { PieceEngine } from '../core/piece/index.js';
|
||||||
|
import { runAgent } from '../agents/runner.js';
|
||||||
|
import {
|
||||||
|
applyDefaultMocks,
|
||||||
|
cleanupPieceEngine,
|
||||||
|
createTestTmpDir,
|
||||||
|
makeMovement,
|
||||||
|
makeResponse,
|
||||||
|
makeRule,
|
||||||
|
mockDetectMatchedRuleSequence,
|
||||||
|
mockRunAgentSequence,
|
||||||
|
} from './engine-test-helpers.js';
|
||||||
|
import type { PieceConfig } from '../core/models/index.js';
|
||||||
|
|
||||||
|
describe('PieceEngine provider_options resolution', () => {
|
||||||
|
let tmpDir: string;
|
||||||
|
let engine: PieceEngine | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
applyDefaultMocks();
|
||||||
|
tmpDir = createTestTmpDir();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (engine) {
|
||||||
|
cleanupPieceEngine(engine);
|
||||||
|
engine = undefined;
|
||||||
|
}
|
||||||
|
if (tmpDir) {
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge provider_options in order: global < project < movement', async () => {
|
||||||
|
const movement = makeMovement('implement', {
|
||||||
|
providerOptions: {
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
claude: { sandbox: { excludedCommands: ['./gradlew'] } },
|
||||||
|
},
|
||||||
|
rules: [makeRule('done', 'COMPLETE')],
|
||||||
|
});
|
||||||
|
|
||||||
|
const config: PieceConfig = {
|
||||||
|
name: 'provider-options-priority',
|
||||||
|
movements: [movement],
|
||||||
|
initialMovement: 'implement',
|
||||||
|
maxMovements: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRunAgentSequence([
|
||||||
|
makeResponse({ persona: movement.persona, content: 'done' }),
|
||||||
|
]);
|
||||||
|
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||||
|
|
||||||
|
engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||||
|
projectCwd: tmpDir,
|
||||||
|
provider: 'claude',
|
||||||
|
globalProviderOptions: {
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
||||||
|
},
|
||||||
|
projectProviderOptions: {
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
|
opencode: { networkAccess: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await engine.run();
|
||||||
|
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
opencode: { networkAccess: true },
|
||||||
|
claude: {
|
||||||
|
sandbox: {
|
||||||
|
allowUnsandboxedCommands: true,
|
||||||
|
excludedCommands: ['./gradlew'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass global provider_options when project and movement options are absent', async () => {
|
||||||
|
const movement = makeMovement('implement', {
|
||||||
|
rules: [makeRule('done', 'COMPLETE')],
|
||||||
|
});
|
||||||
|
|
||||||
|
const config: PieceConfig = {
|
||||||
|
name: 'provider-options-global-only',
|
||||||
|
movements: [movement],
|
||||||
|
initialMovement: 'implement',
|
||||||
|
maxMovements: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRunAgentSequence([
|
||||||
|
makeResponse({ persona: movement.persona, content: 'done' }),
|
||||||
|
]);
|
||||||
|
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||||
|
|
||||||
|
engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||||
|
projectCwd: tmpDir,
|
||||||
|
provider: 'claude',
|
||||||
|
globalProviderOptions: {
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await engine.run();
|
||||||
|
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -39,7 +39,6 @@ describe('loadGlobalConfig', () => {
|
|||||||
const config = loadGlobalConfig();
|
const config = loadGlobalConfig();
|
||||||
|
|
||||||
expect(config.language).toBe('en');
|
expect(config.language).toBe('en');
|
||||||
expect(config.defaultPiece).toBe('default');
|
|
||||||
expect(config.logLevel).toBe('info');
|
expect(config.logLevel).toBe('info');
|
||||||
expect(config.provider).toBe('claude');
|
expect(config.provider).toBe('claude');
|
||||||
expect(config.model).toBeUndefined();
|
expect(config.model).toBeUndefined();
|
||||||
@ -79,6 +78,23 @@ describe('loadGlobalConfig', () => {
|
|||||||
expect(config.logLevel).toBe('debug');
|
expect(config.logLevel).toBe('debug');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should apply env override for nested provider_options key', () => {
|
||||||
|
const original = process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS;
|
||||||
|
try {
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS = 'true';
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
|
const config = loadGlobalConfig();
|
||||||
|
expect(config.providerOptions?.claude?.sandbox?.allowUnsandboxedCommands).toBe(true);
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CLAUDE_SANDBOX_ALLOW_UNSANDBOXED_COMMANDS = original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should load pipeline config from config.yaml', () => {
|
it('should load pipeline config from config.yaml', () => {
|
||||||
const taktDir = join(testHomeDir, '.takt');
|
const taktDir = join(testHomeDir, '.takt');
|
||||||
mkdirSync(taktDir, { recursive: true });
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
|||||||
203
src/__tests__/it-config-provider-options.test.ts
Normal file
203
src/__tests__/it-config-provider-options.test.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
|
vi.mock('../agents/runner.js', () => ({
|
||||||
|
runAgent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../agents/ai-judge.js', async (importOriginal) => {
|
||||||
|
const original = await importOriginal<typeof import('../agents/ai-judge.js')>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../core/piece/phase-runner.js', () => ({
|
||||||
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue({ tag: '', ruleIndex: 0, method: 'auto_select' }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { runAgent } from '../agents/runner.js';
|
||||||
|
import { executeTask } from '../features/tasks/execute/taskExecution.js';
|
||||||
|
import { invalidateGlobalConfigCache } from '../infra/config/index.js';
|
||||||
|
|
||||||
|
interface TestEnv {
|
||||||
|
projectDir: string;
|
||||||
|
globalDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEnv(): TestEnv {
|
||||||
|
const root = join(tmpdir(), `takt-it-config-${randomUUID()}`);
|
||||||
|
const projectDir = join(root, 'project');
|
||||||
|
const globalDir = join(root, 'global');
|
||||||
|
|
||||||
|
mkdirSync(projectDir, { recursive: true });
|
||||||
|
mkdirSync(join(projectDir, '.takt', 'pieces', 'personas'), { recursive: true });
|
||||||
|
mkdirSync(globalDir, { recursive: true });
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(projectDir, '.takt', 'pieces', 'config-it.yaml'),
|
||||||
|
[
|
||||||
|
'name: config-it',
|
||||||
|
'description: config provider options integration test',
|
||||||
|
'max_movements: 3',
|
||||||
|
'initial_movement: plan',
|
||||||
|
'movements:',
|
||||||
|
' - name: plan',
|
||||||
|
' persona: ./personas/planner.md',
|
||||||
|
' instruction: "{task}"',
|
||||||
|
' rules:',
|
||||||
|
' - condition: done',
|
||||||
|
' next: COMPLETE',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
writeFileSync(join(projectDir, '.takt', 'pieces', 'personas', 'planner.md'), 'You are planner.', 'utf-8');
|
||||||
|
|
||||||
|
return { projectDir, globalDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGlobalConfig(globalDir: string, body: string): void {
|
||||||
|
writeFileSync(join(globalDir, 'config.yaml'), body, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProjectConfig(projectDir: string, body: string): void {
|
||||||
|
writeFileSync(join(projectDir, '.takt', 'config.yaml'), body, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDoneResponse() {
|
||||||
|
return {
|
||||||
|
persona: 'planner',
|
||||||
|
status: 'done',
|
||||||
|
content: '[PLAN:1]\ndone',
|
||||||
|
timestamp: new Date(),
|
||||||
|
sessionId: 'session-it',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('IT: config provider_options reflection', () => {
|
||||||
|
let env: TestEnv;
|
||||||
|
let originalConfigDir: string | undefined;
|
||||||
|
let originalEnvCodex: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
env = createEnv();
|
||||||
|
originalConfigDir = process.env.TAKT_CONFIG_DIR;
|
||||||
|
originalEnvCodex = process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
|
||||||
|
process.env.TAKT_CONFIG_DIR = env.globalDir;
|
||||||
|
delete process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
|
vi.mocked(runAgent).mockResolvedValue(makeDoneResponse());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalConfigDir === undefined) {
|
||||||
|
delete process.env.TAKT_CONFIG_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_CONFIG_DIR = originalConfigDir;
|
||||||
|
}
|
||||||
|
if (originalEnvCodex === undefined) {
|
||||||
|
delete process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS = originalEnvCodex;
|
||||||
|
}
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
rmSync(join(env.projectDir, '..'), { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('global provider_options should be passed to runAgent', async () => {
|
||||||
|
setGlobalConfig(
|
||||||
|
env.globalDir,
|
||||||
|
[
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: true',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ok = await executeTask({
|
||||||
|
task: 'test task',
|
||||||
|
cwd: env.projectDir,
|
||||||
|
projectCwd: env.projectDir,
|
||||||
|
pieceIdentifier: 'config-it',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('project provider_options should override global provider_options', async () => {
|
||||||
|
setGlobalConfig(
|
||||||
|
env.globalDir,
|
||||||
|
[
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: true',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
setProjectConfig(
|
||||||
|
env.projectDir,
|
||||||
|
[
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: false',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ok = await executeTask({
|
||||||
|
task: 'test task',
|
||||||
|
cwd: env.projectDir,
|
||||||
|
projectCwd: env.projectDir,
|
||||||
|
pieceIdentifier: 'config-it',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('env provider_options should override yaml provider_options', async () => {
|
||||||
|
setGlobalConfig(
|
||||||
|
env.globalDir,
|
||||||
|
[
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: true',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS = 'false';
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
|
const ok = await executeTask({
|
||||||
|
task: 'test task',
|
||||||
|
cwd: env.projectDir,
|
||||||
|
projectCwd: env.projectDir,
|
||||||
|
pieceIdentifier: 'config-it',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
170
src/__tests__/it-run-config-provider-options.test.ts
Normal file
170
src/__tests__/it-run-config-provider-options.test.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
|
vi.mock('../agents/runner.js', () => ({
|
||||||
|
runAgent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../agents/ai-judge.js', async (importOriginal) => {
|
||||||
|
const original = await importOriginal<typeof import('../agents/ai-judge.js')>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../core/piece/phase-runner.js', () => ({
|
||||||
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue({ tag: '', ruleIndex: 0, method: 'auto_select' }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
|
notifySuccess: vi.fn(),
|
||||||
|
notifyError: vi.fn(),
|
||||||
|
sendSlackNotification: vi.fn(),
|
||||||
|
getSlackWebhookUrl: vi.fn(() => undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { runAllTasks } from '../features/tasks/index.js';
|
||||||
|
import { TaskRunner } from '../infra/task/index.js';
|
||||||
|
import { runAgent } from '../agents/runner.js';
|
||||||
|
import { invalidateGlobalConfigCache } from '../infra/config/index.js';
|
||||||
|
|
||||||
|
interface TestEnv {
|
||||||
|
root: string;
|
||||||
|
projectDir: string;
|
||||||
|
globalDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEnv(): TestEnv {
|
||||||
|
const root = join(tmpdir(), `takt-it-run-config-${randomUUID()}`);
|
||||||
|
const projectDir = join(root, 'project');
|
||||||
|
const globalDir = join(root, 'global');
|
||||||
|
|
||||||
|
mkdirSync(join(projectDir, '.takt', 'pieces', 'personas'), { recursive: true });
|
||||||
|
mkdirSync(globalDir, { recursive: true });
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(projectDir, '.takt', 'pieces', 'run-config-it.yaml'),
|
||||||
|
[
|
||||||
|
'name: run-config-it',
|
||||||
|
'description: run config provider options integration test',
|
||||||
|
'max_movements: 3',
|
||||||
|
'initial_movement: plan',
|
||||||
|
'movements:',
|
||||||
|
' - name: plan',
|
||||||
|
' persona: ./personas/planner.md',
|
||||||
|
' instruction: "{task}"',
|
||||||
|
' rules:',
|
||||||
|
' - condition: done',
|
||||||
|
' next: COMPLETE',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
writeFileSync(join(projectDir, '.takt', 'pieces', 'personas', 'planner.md'), 'You are planner.', 'utf-8');
|
||||||
|
|
||||||
|
return { root, projectDir, globalDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGlobalConfig(globalDir: string, body: string): void {
|
||||||
|
writeFileSync(join(globalDir, 'config.yaml'), body, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProjectConfig(projectDir: string, body: string): void {
|
||||||
|
writeFileSync(join(projectDir, '.takt', 'config.yaml'), body, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockDoneResponse() {
|
||||||
|
return {
|
||||||
|
persona: 'planner',
|
||||||
|
status: 'done',
|
||||||
|
content: '[PLAN:1]\ndone',
|
||||||
|
timestamp: new Date(),
|
||||||
|
sessionId: 'session-it',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('IT: runAllTasks provider_options reflection', () => {
|
||||||
|
let env: TestEnv;
|
||||||
|
let originalConfigDir: string | undefined;
|
||||||
|
let originalEnvCodex: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
env = createEnv();
|
||||||
|
originalConfigDir = process.env.TAKT_CONFIG_DIR;
|
||||||
|
originalEnvCodex = process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
process.env.TAKT_CONFIG_DIR = env.globalDir;
|
||||||
|
delete process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
|
vi.mocked(runAgent).mockResolvedValue(mockDoneResponse());
|
||||||
|
|
||||||
|
const runner = new TaskRunner(env.projectDir);
|
||||||
|
runner.addTask('test task');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalConfigDir === undefined) {
|
||||||
|
delete process.env.TAKT_CONFIG_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_CONFIG_DIR = originalConfigDir;
|
||||||
|
}
|
||||||
|
if (originalEnvCodex === undefined) {
|
||||||
|
delete process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS;
|
||||||
|
} else {
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS = originalEnvCodex;
|
||||||
|
}
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
rmSync(env.root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('project provider_options should override global in runAllTasks flow', async () => {
|
||||||
|
setGlobalConfig(env.globalDir, [
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: true',
|
||||||
|
].join('\n'));
|
||||||
|
setProjectConfig(env.projectDir, [
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: false',
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
await runAllTasks(env.projectDir, 'run-config-it');
|
||||||
|
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('env provider_options should override yaml in runAllTasks flow', async () => {
|
||||||
|
setGlobalConfig(env.globalDir, [
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: false',
|
||||||
|
].join('\n'));
|
||||||
|
setProjectConfig(env.projectDir, [
|
||||||
|
'provider_options:',
|
||||||
|
' codex:',
|
||||||
|
' network_access: false',
|
||||||
|
].join('\n'));
|
||||||
|
process.env.TAKT_PROVIDER_OPTIONS_CODEX_NETWORK_ACCESS = 'true';
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
|
await runAllTasks(env.projectDir, 'run-config-it');
|
||||||
|
|
||||||
|
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
|
||||||
|
expect(options?.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -495,7 +495,6 @@ describe('GlobalConfigSchema', () => {
|
|||||||
const config = {};
|
const config = {};
|
||||||
const result = GlobalConfigSchema.parse(config);
|
const result = GlobalConfigSchema.parse(config);
|
||||||
|
|
||||||
expect(result.default_piece).toBe('default');
|
|
||||||
expect(result.log_level).toBe('info');
|
expect(result.log_level).toBe('info');
|
||||||
expect(result.provider).toBe('claude');
|
expect(result.provider).toBe('claude');
|
||||||
expect(result.observability).toBeUndefined();
|
expect(result.observability).toBeUndefined();
|
||||||
@ -503,7 +502,6 @@ describe('GlobalConfigSchema', () => {
|
|||||||
|
|
||||||
it('should accept valid config', () => {
|
it('should accept valid config', () => {
|
||||||
const config = {
|
const config = {
|
||||||
default_piece: 'custom',
|
|
||||||
log_level: 'debug' as const,
|
log_level: 'debug' as const,
|
||||||
observability: {
|
observability: {
|
||||||
provider_events: false,
|
provider_events: false,
|
||||||
|
|||||||
@ -2,8 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
getProviderMock,
|
getProviderMock,
|
||||||
loadProjectConfigMock,
|
loadConfigMock,
|
||||||
loadGlobalConfigMock,
|
|
||||||
loadCustomAgentsMock,
|
loadCustomAgentsMock,
|
||||||
loadAgentPromptMock,
|
loadAgentPromptMock,
|
||||||
loadTemplateMock,
|
loadTemplateMock,
|
||||||
@ -15,8 +14,7 @@ const {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getProviderMock: vi.fn(() => ({ setup: providerSetup })),
|
getProviderMock: vi.fn(() => ({ setup: providerSetup })),
|
||||||
loadProjectConfigMock: vi.fn(),
|
loadConfigMock: vi.fn(),
|
||||||
loadGlobalConfigMock: vi.fn(),
|
|
||||||
loadCustomAgentsMock: vi.fn(),
|
loadCustomAgentsMock: vi.fn(),
|
||||||
loadAgentPromptMock: vi.fn(),
|
loadAgentPromptMock: vi.fn(),
|
||||||
loadTemplateMock: vi.fn(),
|
loadTemplateMock: vi.fn(),
|
||||||
@ -30,8 +28,7 @@ vi.mock('../infra/providers/index.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
loadProjectConfig: loadProjectConfigMock,
|
loadConfig: loadConfigMock,
|
||||||
loadGlobalConfig: loadGlobalConfigMock,
|
|
||||||
loadCustomAgents: loadCustomAgentsMock,
|
loadCustomAgents: loadCustomAgentsMock,
|
||||||
loadAgentPrompt: loadAgentPromptMock,
|
loadAgentPrompt: loadAgentPromptMock,
|
||||||
}));
|
}));
|
||||||
@ -47,8 +44,7 @@ describe('option resolution order', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
providerCallMock.mockResolvedValue({ content: 'ok' });
|
providerCallMock.mockResolvedValue({ content: 'ok' });
|
||||||
loadProjectConfigMock.mockReturnValue({});
|
loadConfigMock.mockReturnValue({ global: {}, project: {} });
|
||||||
loadGlobalConfigMock.mockReturnValue({});
|
|
||||||
loadCustomAgentsMock.mockReturnValue(new Map());
|
loadCustomAgentsMock.mockReturnValue(new Map());
|
||||||
loadAgentPromptMock.mockReturnValue('prompt');
|
loadAgentPromptMock.mockReturnValue('prompt');
|
||||||
loadTemplateMock.mockReturnValue('template');
|
loadTemplateMock.mockReturnValue('template');
|
||||||
@ -56,8 +52,10 @@ describe('option resolution order', () => {
|
|||||||
|
|
||||||
it('should resolve provider in order: CLI > Local > Piece(step) > Global', async () => {
|
it('should resolve provider in order: CLI > Local > Piece(step) > Global', async () => {
|
||||||
// Given
|
// Given
|
||||||
loadProjectConfigMock.mockReturnValue({ provider: 'opencode' });
|
loadConfigMock.mockReturnValue({
|
||||||
loadGlobalConfigMock.mockReturnValue({ provider: 'mock' });
|
project: { provider: 'opencode' },
|
||||||
|
global: { provider: 'mock' },
|
||||||
|
});
|
||||||
|
|
||||||
// When: CLI provider が指定される
|
// When: CLI provider が指定される
|
||||||
await runAgent(undefined, 'task', {
|
await runAgent(undefined, 'task', {
|
||||||
@ -79,7 +77,10 @@ describe('option resolution order', () => {
|
|||||||
expect(getProviderMock).toHaveBeenLastCalledWith('opencode');
|
expect(getProviderMock).toHaveBeenLastCalledWith('opencode');
|
||||||
|
|
||||||
// When: Local なし(Piece が有効)
|
// When: Local なし(Piece が有効)
|
||||||
loadProjectConfigMock.mockReturnValue({});
|
loadConfigMock.mockReturnValue({
|
||||||
|
project: {},
|
||||||
|
global: { provider: 'mock' },
|
||||||
|
});
|
||||||
await runAgent(undefined, 'task', {
|
await runAgent(undefined, 'task', {
|
||||||
cwd: '/repo',
|
cwd: '/repo',
|
||||||
stepProvider: 'claude',
|
stepProvider: 'claude',
|
||||||
@ -97,8 +98,10 @@ describe('option resolution order', () => {
|
|||||||
|
|
||||||
it('should resolve model in order: CLI > Piece(step) > Global(matching provider)', async () => {
|
it('should resolve model in order: CLI > Piece(step) > Global(matching provider)', async () => {
|
||||||
// Given
|
// Given
|
||||||
loadProjectConfigMock.mockReturnValue({ provider: 'claude' });
|
loadConfigMock.mockReturnValue({
|
||||||
loadGlobalConfigMock.mockReturnValue({ provider: 'claude', model: 'global-model' });
|
project: { provider: 'claude' },
|
||||||
|
global: { provider: 'claude', model: 'global-model' },
|
||||||
|
});
|
||||||
|
|
||||||
// When: CLI model あり
|
// When: CLI model あり
|
||||||
await runAgent(undefined, 'task', {
|
await runAgent(undefined, 'task', {
|
||||||
@ -137,8 +140,10 @@ describe('option resolution order', () => {
|
|||||||
|
|
||||||
it('should ignore global model when global provider does not match resolved provider', async () => {
|
it('should ignore global model when global provider does not match resolved provider', async () => {
|
||||||
// Given
|
// Given
|
||||||
loadProjectConfigMock.mockReturnValue({ provider: 'codex' });
|
loadConfigMock.mockReturnValue({
|
||||||
loadGlobalConfigMock.mockReturnValue({ provider: 'claude', model: 'global-model' });
|
project: { provider: 'codex' },
|
||||||
|
global: { provider: 'claude', model: 'global-model' },
|
||||||
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await runAgent(undefined, 'task', { cwd: '/repo' });
|
await runAgent(undefined, 'task', { cwd: '/repo' });
|
||||||
@ -160,17 +165,16 @@ describe('option resolution order', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
loadProjectConfigMock.mockReturnValue({
|
loadConfigMock.mockReturnValue({
|
||||||
|
project: {
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
provider_options: {
|
|
||||||
claude: { sandbox: { allow_unsandboxed_commands: true } },
|
|
||||||
},
|
},
|
||||||
});
|
global: {
|
||||||
loadGlobalConfigMock.mockReturnValue({
|
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|||||||
@ -69,6 +69,56 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
|||||||
const options = builder.buildBaseOptions(step);
|
const options = builder.buildBaseOptions(step);
|
||||||
expect(options.permissionMode).toBe('edit');
|
expect(options.permissionMode).toBe('edit');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges provider options with precedence: global < project < movement', () => {
|
||||||
|
const step = createMovement({
|
||||||
|
providerOptions: {
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
claude: { sandbox: { excludedCommands: ['./gradlew'] } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const builder = createBuilder(step, {
|
||||||
|
globalProviderOptions: {
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
||||||
|
},
|
||||||
|
projectProviderOptions: {
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
|
opencode: { networkAccess: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = builder.buildBaseOptions(step);
|
||||||
|
|
||||||
|
expect(options.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
opencode: { networkAccess: true },
|
||||||
|
claude: {
|
||||||
|
sandbox: {
|
||||||
|
allowUnsandboxedCommands: true,
|
||||||
|
excludedCommands: ['./gradlew'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to global/project provider options when movement has none', () => {
|
||||||
|
const step = createMovement();
|
||||||
|
const builder = createBuilder(step, {
|
||||||
|
globalProviderOptions: {
|
||||||
|
codex: { networkAccess: true },
|
||||||
|
},
|
||||||
|
projectProviderOptions: {
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = builder.buildBaseOptions(step);
|
||||||
|
|
||||||
|
expect(options.providerOptions).toEqual({
|
||||||
|
codex: { networkAccess: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('OptionsBuilder.buildResumeOptions', () => {
|
describe('OptionsBuilder.buildResumeOptions', () => {
|
||||||
|
|||||||
@ -5,25 +5,33 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import type { TaskInfo } from '../infra/task/index.js';
|
import type { TaskInfo } from '../infra/task/index.js';
|
||||||
|
|
||||||
// Mock dependencies before importing the module under test
|
const { mockLoadConfigRaw } = vi.hoisted(() => ({
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
mockLoadConfigRaw: vi.fn(() => ({
|
||||||
loadPieceByIdentifier: vi.fn(),
|
|
||||||
isPiecePath: vi.fn(() => false),
|
|
||||||
loadGlobalConfig: vi.fn(() => ({
|
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
taskPollIntervalMs: 500,
|
taskPollIntervalMs: 500,
|
||||||
})),
|
})),
|
||||||
loadProjectConfig: vi.fn(() => ({
|
|
||||||
piece: 'default',
|
|
||||||
permissionMode: 'default',
|
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { loadGlobalConfig } from '../infra/config/index.js';
|
// Mock dependencies before importing the module under test
|
||||||
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
|
loadPieceByIdentifier: vi.fn(),
|
||||||
|
isPiecePath: vi.fn(() => false),
|
||||||
|
loadConfig: (...args: unknown[]) => {
|
||||||
|
const raw = mockLoadConfigRaw(...args) as Record<string, unknown>;
|
||||||
|
if ('global' in raw && 'project' in raw) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
global: raw,
|
||||||
|
project: { piece: 'default' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockLoadConfig = mockLoadConfigRaw;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mockClaimNextTasks,
|
mockClaimNextTasks,
|
||||||
@ -167,7 +175,7 @@ beforeEach(() => {
|
|||||||
describe('runAllTasks concurrency', () => {
|
describe('runAllTasks concurrency', () => {
|
||||||
describe('sequential execution (concurrency=1)', () => {
|
describe('sequential execution (concurrency=1)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -210,7 +218,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
|
|
||||||
describe('parallel execution (concurrency>1)', () => {
|
describe('parallel execution (concurrency>1)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -288,7 +296,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
describe('default concurrency', () => {
|
describe('default concurrency', () => {
|
||||||
it('should default to sequential when concurrency is not set', async () => {
|
it('should default to sequential when concurrency is not set', async () => {
|
||||||
// Given: Config without explicit concurrency (defaults to 1)
|
// Given: Config without explicit concurrency (defaults to 1)
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -324,7 +332,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -371,7 +379,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
|
|
||||||
it('should fill slots immediately when a task completes (no batch waiting)', async () => {
|
it('should fill slots immediately when a task completes (no batch waiting)', async () => {
|
||||||
// Given: 3 tasks, concurrency=2, task1 finishes quickly, task2 takes longer
|
// Given: 3 tasks, concurrency=2, task1 finishes quickly, task2 takes longer
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -413,7 +421,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
|
|
||||||
it('should count partial failures correctly', async () => {
|
it('should count partial failures correctly', async () => {
|
||||||
// Given: 3 tasks, 1 fails, 2 succeed
|
// Given: 3 tasks, 1 fails, 2 succeed
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -495,7 +503,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
|
|
||||||
it('should pass abortSignal but not taskPrefix in sequential mode', async () => {
|
it('should pass abortSignal but not taskPrefix in sequential mode', async () => {
|
||||||
// Given: Sequential mode
|
// Given: Sequential mode
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -525,7 +533,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only notify once at run completion when multiple tasks succeed', async () => {
|
it('should only notify once at run completion when multiple tasks succeed', async () => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -550,7 +558,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not notify run completion when runComplete is explicitly false', async () => {
|
it('should not notify run completion when runComplete is explicitly false', async () => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -572,7 +580,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should notify run completion by default when notification_sound_events is not set', async () => {
|
it('should notify run completion by default when notification_sound_events is not set', async () => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -594,7 +602,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should notify run abort by default when notification_sound_events is not set', async () => {
|
it('should notify run abort by default when notification_sound_events is not set', async () => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -617,7 +625,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not notify run abort when runAbort is explicitly false', async () => {
|
it('should not notify run abort when runAbort is explicitly false', async () => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -640,7 +648,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should notify run abort and rethrow when worker pool throws', async () => {
|
it('should notify run abort and rethrow when worker pool throws', async () => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@ -675,7 +683,7 @@ describe('runAllTasks concurrency', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLoadGlobalConfig.mockReturnValue({
|
mockLoadConfig.mockReturnValue({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
defaultPiece: 'default',
|
defaultPiece: 'default',
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
|
|||||||
@ -32,7 +32,7 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
listPieces: vi.fn(() => ['default']),
|
listPieces: vi.fn(() => ['default']),
|
||||||
listPieceEntries: vi.fn(() => []),
|
listPieceEntries: vi.fn(() => []),
|
||||||
isPiecePath: vi.fn(() => false),
|
isPiecePath: vi.fn(() => false),
|
||||||
loadGlobalConfig: vi.fn(() => ({})),
|
loadConfig: vi.fn(() => ({ global: {}, project: {} })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/index.js', () => ({
|
vi.mock('../infra/task/index.js', () => ({
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import type { TaskInfo } from '../infra/task/index.js';
|
import type { TaskInfo } from '../infra/task/index.js';
|
||||||
|
|
||||||
const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockLoadGlobalConfig, mockLoadProjectConfig, mockBuildTaskResult, mockPersistTaskResult, mockPostExecutionFlow } =
|
const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockLoadGlobalConfig, mockLoadProjectConfig, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } =
|
||||||
vi.hoisted(() => ({
|
vi.hoisted(() => ({
|
||||||
mockResolveTaskExecution: vi.fn(),
|
mockResolveTaskExecution: vi.fn(),
|
||||||
mockExecutePiece: vi.fn(),
|
mockExecutePiece: vi.fn(),
|
||||||
@ -14,6 +14,7 @@ const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, m
|
|||||||
mockLoadProjectConfig: vi.fn(),
|
mockLoadProjectConfig: vi.fn(),
|
||||||
mockBuildTaskResult: vi.fn(),
|
mockBuildTaskResult: vi.fn(),
|
||||||
mockPersistTaskResult: vi.fn(),
|
mockPersistTaskResult: vi.fn(),
|
||||||
|
mockPersistTaskError: vi.fn(),
|
||||||
mockPostExecutionFlow: vi.fn(),
|
mockPostExecutionFlow: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ vi.mock('../features/tasks/execute/pieceExecution.js', () => ({
|
|||||||
vi.mock('../features/tasks/execute/taskResultHandler.js', () => ({
|
vi.mock('../features/tasks/execute/taskResultHandler.js', () => ({
|
||||||
buildTaskResult: (...args: unknown[]) => mockBuildTaskResult(...args),
|
buildTaskResult: (...args: unknown[]) => mockBuildTaskResult(...args),
|
||||||
persistTaskResult: (...args: unknown[]) => mockPersistTaskResult(...args),
|
persistTaskResult: (...args: unknown[]) => mockPersistTaskResult(...args),
|
||||||
|
persistTaskError: (...args: unknown[]) => mockPersistTaskError(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/tasks/execute/postExecution.js', () => ({
|
vi.mock('../features/tasks/execute/postExecution.js', () => ({
|
||||||
@ -37,8 +39,10 @@ vi.mock('../features/tasks/execute/postExecution.js', () => ({
|
|||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||||
isPiecePath: () => false,
|
isPiecePath: () => false,
|
||||||
loadGlobalConfig: () => mockLoadGlobalConfig(),
|
loadConfig: () => ({
|
||||||
loadProjectConfig: () => mockLoadProjectConfig(),
|
global: mockLoadGlobalConfig(),
|
||||||
|
project: mockLoadProjectConfig(),
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -88,10 +92,16 @@ describe('executeAndCompleteTask', () => {
|
|||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
personaProviders: {},
|
personaProviders: {},
|
||||||
providerProfiles: {},
|
providerProfiles: {},
|
||||||
|
providerOptions: {
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
mockLoadProjectConfig.mockReturnValue({
|
mockLoadProjectConfig.mockReturnValue({
|
||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
providerProfiles: {},
|
providerProfiles: {},
|
||||||
|
providerOptions: {
|
||||||
|
opencode: { networkAccess: true },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
mockBuildTaskResult.mockReturnValue({ success: true });
|
mockBuildTaskResult.mockReturnValue({ success: true });
|
||||||
mockResolveTaskExecution.mockResolvedValue({
|
mockResolveTaskExecution.mockResolvedValue({
|
||||||
@ -130,8 +140,16 @@ describe('executeAndCompleteTask', () => {
|
|||||||
const pieceExecutionOptions = mockExecutePiece.mock.calls[0]?.[3] as {
|
const pieceExecutionOptions = mockExecutePiece.mock.calls[0]?.[3] as {
|
||||||
taskDisplayLabel?: string;
|
taskDisplayLabel?: string;
|
||||||
taskPrefix?: string;
|
taskPrefix?: string;
|
||||||
|
globalProviderOptions?: unknown;
|
||||||
|
projectProviderOptions?: unknown;
|
||||||
};
|
};
|
||||||
expect(pieceExecutionOptions?.taskDisplayLabel).toBe(taskDisplayLabel);
|
expect(pieceExecutionOptions?.taskDisplayLabel).toBe(taskDisplayLabel);
|
||||||
expect(pieceExecutionOptions?.taskPrefix).toBe(taskDisplayLabel);
|
expect(pieceExecutionOptions?.taskPrefix).toBe(taskDisplayLabel);
|
||||||
|
expect(pieceExecutionOptions?.globalProviderOptions).toEqual({
|
||||||
|
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||||
|
});
|
||||||
|
expect(pieceExecutionOptions?.projectProviderOptions).toEqual({
|
||||||
|
opencode: { networkAccess: true },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { basename, dirname } from 'node:path';
|
import { basename, dirname } from 'node:path';
|
||||||
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
import { loadCustomAgents, loadAgentPrompt, loadConfig } from '../infra/config/index.js';
|
||||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||||
import { createLogger } from '../shared/utils/index.js';
|
import { createLogger } from '../shared/utils/index.js';
|
||||||
@ -29,12 +29,13 @@ export class AgentRunner {
|
|||||||
agentConfig?: CustomAgentConfig,
|
agentConfig?: CustomAgentConfig,
|
||||||
): ProviderType {
|
): ProviderType {
|
||||||
if (options?.provider) return options.provider;
|
if (options?.provider) return options.provider;
|
||||||
const projectConfig = loadProjectConfig(cwd);
|
const config = loadConfig(cwd);
|
||||||
|
const projectConfig = config.project;
|
||||||
if (projectConfig.provider) return projectConfig.provider;
|
if (projectConfig.provider) return projectConfig.provider;
|
||||||
if (options?.stepProvider) return options.stepProvider;
|
if (options?.stepProvider) return options.stepProvider;
|
||||||
if (agentConfig?.provider) return agentConfig.provider;
|
if (agentConfig?.provider) return agentConfig.provider;
|
||||||
try {
|
try {
|
||||||
const globalConfig = loadGlobalConfig();
|
const globalConfig = config.global;
|
||||||
if (globalConfig.provider) return globalConfig.provider;
|
if (globalConfig.provider) return globalConfig.provider;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.debug('Global config not available for provider resolution', { error });
|
log.debug('Global config not available for provider resolution', { error });
|
||||||
@ -55,8 +56,9 @@ export class AgentRunner {
|
|||||||
if (options?.model) return options.model;
|
if (options?.model) return options.model;
|
||||||
if (options?.stepModel) return options.stepModel;
|
if (options?.stepModel) return options.stepModel;
|
||||||
if (agentConfig?.model) return agentConfig.model;
|
if (agentConfig?.model) return agentConfig.model;
|
||||||
|
if (!options?.cwd) return undefined;
|
||||||
try {
|
try {
|
||||||
const globalConfig = loadGlobalConfig();
|
const globalConfig = loadConfig(options.cwd).global;
|
||||||
if (globalConfig.model) {
|
if (globalConfig.model) {
|
||||||
const globalProvider = globalConfig.provider ?? 'claude';
|
const globalProvider = globalConfig.provider ?? 'claude';
|
||||||
if (globalProvider === resolvedProvider) return globalConfig.model;
|
if (globalProvider === resolvedProvider) return globalConfig.model;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* CLI subcommand definitions
|
* CLI subcommand definitions
|
||||||
*
|
*
|
||||||
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config, prompt, catalog).
|
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, prompt, catalog).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { clearPersonaSessions, getCurrentPiece } from '../../infra/config/index.js';
|
import { clearPersonaSessions, getCurrentPiece } from '../../infra/config/index.js';
|
||||||
import { success } from '../../shared/ui/index.js';
|
import { success } from '../../shared/ui/index.js';
|
||||||
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
|
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
|
||||||
import { switchPiece, switchConfig, ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES, resetCategoriesToDefault, deploySkill } from '../../features/config/index.js';
|
import { switchPiece, ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES, resetCategoriesToDefault, deploySkill } from '../../features/config/index.js';
|
||||||
import { previewPrompts } from '../../features/prompt/index.js';
|
import { previewPrompts } from '../../features/prompt/index.js';
|
||||||
import { showCatalog } from '../../features/catalog/index.js';
|
import { showCatalog } from '../../features/catalog/index.js';
|
||||||
import { program, resolvedCwd } from './program.js';
|
import { program, resolvedCwd } from './program.js';
|
||||||
@ -96,14 +96,6 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
|
||||||
.command('config')
|
|
||||||
.description('Configure settings (permission mode)')
|
|
||||||
.argument('[key]', 'Configuration key')
|
|
||||||
.action(async (key?: string) => {
|
|
||||||
await switchConfig(resolvedCwd, key);
|
|
||||||
});
|
|
||||||
|
|
||||||
const reset = program
|
const reset = program
|
||||||
.command('reset')
|
.command('reset')
|
||||||
.description('Reset settings to defaults');
|
.description('Reset settings to defaults');
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { resolve } from 'node:path';
|
|||||||
import {
|
import {
|
||||||
initGlobalDirs,
|
initGlobalDirs,
|
||||||
initProjectDirs,
|
initProjectDirs,
|
||||||
loadGlobalConfig,
|
loadConfig,
|
||||||
isVerboseMode,
|
isVerboseMode,
|
||||||
} from '../../infra/config/index.js';
|
} from '../../infra/config/index.js';
|
||||||
import { setQuietMode } from '../../shared/context.js';
|
import { setQuietMode } from '../../shared/context.js';
|
||||||
@ -69,7 +69,7 @@ export async function runPreActionHook(): Promise<void> {
|
|||||||
const verbose = isVerboseMode(resolvedCwd);
|
const verbose = isVerboseMode(resolvedCwd);
|
||||||
initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd);
|
initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd);
|
||||||
|
|
||||||
const config = loadGlobalConfig();
|
const { global: config } = loadConfig(resolvedCwd);
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
setVerboseConsole(true);
|
setVerboseConsole(true);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
dispatchConversationAction,
|
dispatchConversationAction,
|
||||||
type InteractiveModeResult,
|
type InteractiveModeResult,
|
||||||
} from '../../features/interactive/index.js';
|
} from '../../features/interactive/index.js';
|
||||||
import { getPieceDescription, loadGlobalConfig } from '../../infra/config/index.js';
|
import { getPieceDescription, loadConfig } from '../../infra/config/index.js';
|
||||||
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
||||||
import { program, resolvedCwd, pipelineMode } from './program.js';
|
import { program, resolvedCwd, pipelineMode } from './program.js';
|
||||||
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
||||||
@ -137,7 +137,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All paths below go through interactive mode
|
// All paths below go through interactive mode
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(resolvedCwd);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
||||||
|
|||||||
@ -53,7 +53,6 @@ export interface NotificationSoundEventsConfig {
|
|||||||
/** Global configuration for takt */
|
/** Global configuration for takt */
|
||||||
export interface GlobalConfig {
|
export interface GlobalConfig {
|
||||||
language: Language;
|
language: Language;
|
||||||
defaultPiece: string;
|
|
||||||
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
||||||
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
||||||
model?: string;
|
model?: string;
|
||||||
@ -100,6 +99,8 @@ export interface GlobalConfig {
|
|||||||
notificationSoundEvents?: NotificationSoundEventsConfig;
|
notificationSoundEvents?: NotificationSoundEventsConfig;
|
||||||
/** Number of movement previews to inject into interactive mode (0 to disable, max 10) */
|
/** Number of movement previews to inject into interactive mode (0 to disable, max 10) */
|
||||||
interactivePreviewMovements?: number;
|
interactivePreviewMovements?: number;
|
||||||
|
/** Verbose output mode */
|
||||||
|
verbose?: boolean;
|
||||||
/** Number of tasks to run concurrently in takt run (default: 1 = sequential) */
|
/** Number of tasks to run concurrently in takt run (default: 1 = sequential) */
|
||||||
concurrency: number;
|
concurrency: number;
|
||||||
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
|
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
|
||||||
|
|||||||
@ -405,7 +405,6 @@ export const PieceCategoryConfigSchema = z.record(z.string(), PieceCategoryConfi
|
|||||||
/** Global config schema */
|
/** Global config schema */
|
||||||
export const GlobalConfigSchema = z.object({
|
export const GlobalConfigSchema = z.object({
|
||||||
language: LanguageSchema.optional().default(DEFAULT_LANGUAGE),
|
language: LanguageSchema.optional().default(DEFAULT_LANGUAGE),
|
||||||
default_piece: z.string().optional().default('default'),
|
|
||||||
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
|
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
|
||||||
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional().default('claude'),
|
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional().default('claude'),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
@ -458,6 +457,8 @@ export const GlobalConfigSchema = z.object({
|
|||||||
}).optional(),
|
}).optional(),
|
||||||
/** Number of movement previews to inject into interactive mode (0 to disable, max 10) */
|
/** Number of movement previews to inject into interactive mode (0 to disable, max 10) */
|
||||||
interactive_preview_movements: z.number().int().min(0).max(10).optional().default(3),
|
interactive_preview_movements: z.number().int().min(0).max(10).optional().default(3),
|
||||||
|
/** Verbose output mode */
|
||||||
|
verbose: z.boolean().optional(),
|
||||||
/** Number of tasks to run concurrently in takt run (default: 1 = sequential, max: 10) */
|
/** Number of tasks to run concurrently in takt run (default: 1 = sequential, max: 10) */
|
||||||
concurrency: z.number().int().min(1).max(10).optional().default(1),
|
concurrency: z.number().int().min(1).max(10).optional().default(1),
|
||||||
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
|
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import type { PieceMovement, PieceState, Language } from '../../models/types.js';
|
import type { PieceMovement, PieceState, Language } from '../../models/types.js';
|
||||||
|
import type { MovementProviderOptions } from '../../models/piece-types.js';
|
||||||
import type { RunAgentOptions } from '../../../agents/runner.js';
|
import type { RunAgentOptions } from '../../../agents/runner.js';
|
||||||
import type { PhaseRunnerContext } from '../phase-runner.js';
|
import type { PhaseRunnerContext } from '../phase-runner.js';
|
||||||
import type { PieceEngineOptions, PhaseName } from '../types.js';
|
import type { PieceEngineOptions, PhaseName } from '../types.js';
|
||||||
@ -7,6 +8,27 @@ import { buildSessionKey } from '../session-key.js';
|
|||||||
import { resolveMovementProviderModel } from '../provider-resolution.js';
|
import { resolveMovementProviderModel } from '../provider-resolution.js';
|
||||||
import { DEFAULT_PROVIDER_PERMISSION_PROFILES, resolveMovementPermissionMode } from '../permission-profile-resolution.js';
|
import { DEFAULT_PROVIDER_PERMISSION_PROFILES, resolveMovementPermissionMode } from '../permission-profile-resolution.js';
|
||||||
|
|
||||||
|
function mergeProviderOptions(
|
||||||
|
...layers: (MovementProviderOptions | undefined)[]
|
||||||
|
): MovementProviderOptions | undefined {
|
||||||
|
const result: MovementProviderOptions = {};
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (!layer) continue;
|
||||||
|
if (layer.codex) {
|
||||||
|
result.codex = { ...result.codex, ...layer.codex };
|
||||||
|
}
|
||||||
|
if (layer.opencode) {
|
||||||
|
result.opencode = { ...result.opencode, ...layer.opencode };
|
||||||
|
}
|
||||||
|
if (layer.claude?.sandbox) {
|
||||||
|
result.claude = {
|
||||||
|
sandbox: { ...result.claude?.sandbox, ...layer.claude.sandbox },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export class OptionsBuilder {
|
export class OptionsBuilder {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly engineOptions: PieceEngineOptions,
|
private readonly engineOptions: PieceEngineOptions,
|
||||||
@ -54,7 +76,11 @@ export class OptionsBuilder {
|
|||||||
projectProviderProfiles: this.engineOptions.projectProviderProfiles,
|
projectProviderProfiles: this.engineOptions.projectProviderProfiles,
|
||||||
globalProviderProfiles: this.engineOptions.globalProviderProfiles ?? DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
globalProviderProfiles: this.engineOptions.globalProviderProfiles ?? DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
||||||
}),
|
}),
|
||||||
providerOptions: step.providerOptions,
|
providerOptions: mergeProviderOptions(
|
||||||
|
this.engineOptions.globalProviderOptions,
|
||||||
|
this.engineOptions.projectProviderOptions,
|
||||||
|
step.providerOptions,
|
||||||
|
),
|
||||||
language: this.getLanguage(),
|
language: this.getLanguage(),
|
||||||
onStream: this.engineOptions.onStream,
|
onStream: this.engineOptions.onStream,
|
||||||
onPermissionRequest: this.engineOptions.onPermissionRequest,
|
onPermissionRequest: this.engineOptions.onPermissionRequest,
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
|
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import type { PieceMovement, AgentResponse, PieceState, Language, LoopMonitorConfig } from '../models/types.js';
|
import type { PieceMovement, AgentResponse, PieceState, Language, LoopMonitorConfig } from '../models/types.js';
|
||||||
import type { ProviderPermissionProfiles } from '../models/provider-profiles.js';
|
import type { ProviderPermissionProfiles } from '../models/provider-profiles.js';
|
||||||
|
import type { MovementProviderOptions } from '../models/piece-types.js';
|
||||||
|
|
||||||
export type ProviderType = 'claude' | 'codex' | 'opencode' | 'mock';
|
export type ProviderType = 'claude' | 'codex' | 'opencode' | 'mock';
|
||||||
|
|
||||||
@ -171,7 +172,7 @@ export interface PieceEngineOptions {
|
|||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
/** Callback when iteration limit is reached - returns additional iterations or null to stop */
|
/** Callback when iteration limit is reached - returns additional iterations or null to stop */
|
||||||
onIterationLimit?: IterationLimitCallback;
|
onIterationLimit?: IterationLimitCallback;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks */
|
||||||
bypassPermissions?: boolean;
|
bypassPermissions?: boolean;
|
||||||
/** Project root directory (where .takt/ lives). */
|
/** Project root directory (where .takt/ lives). */
|
||||||
projectCwd: string;
|
projectCwd: string;
|
||||||
@ -183,6 +184,10 @@ export interface PieceEngineOptions {
|
|||||||
/** Global config provider (used for provider/profile resolution parity with AgentRunner) */
|
/** Global config provider (used for provider/profile resolution parity with AgentRunner) */
|
||||||
globalProvider?: ProviderType;
|
globalProvider?: ProviderType;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** Project-level provider options */
|
||||||
|
projectProviderOptions?: MovementProviderOptions;
|
||||||
|
/** Global-level provider options */
|
||||||
|
globalProviderOptions?: MovementProviderOptions;
|
||||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||||
personaProviders?: Record<string, ProviderType>;
|
personaProviders?: Record<string, ProviderType>;
|
||||||
/** Project-level provider permission profiles */
|
/** Project-level provider permission profiles */
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { switchPiece } from './switchPiece.js';
|
export { switchPiece } from './switchPiece.js';
|
||||||
export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './switchConfig.js';
|
|
||||||
export { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES } from './ejectBuiltin.js';
|
export { ejectBuiltin, ejectFacet, parseFacetType, VALID_FACET_TYPES } from './ejectBuiltin.js';
|
||||||
export { resetCategoriesToDefault } from './resetCategories.js';
|
export { resetCategoriesToDefault } from './resetCategories.js';
|
||||||
export { deploySkill } from './deploySkill.js';
|
export { deploySkill } from './deploySkill.js';
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
/**
|
|
||||||
* Config switching command (like piece switching)
|
|
||||||
*
|
|
||||||
* Permission mode selection that works from CLI.
|
|
||||||
* Uses selectOption for prompt selection, same pattern as switchPiece.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { info, success } from '../../shared/ui/index.js';
|
|
||||||
import { selectOption } from '../../shared/prompt/index.js';
|
|
||||||
import {
|
|
||||||
loadProjectConfig,
|
|
||||||
updateProjectConfig,
|
|
||||||
} from '../../infra/config/index.js';
|
|
||||||
import type { PermissionMode } from '../../infra/config/index.js';
|
|
||||||
|
|
||||||
// Re-export for convenience
|
|
||||||
export type { PermissionMode } from '../../infra/config/index.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get permission mode options for selection
|
|
||||||
*/
|
|
||||||
/** Common permission mode option definitions */
|
|
||||||
export const PERMISSION_MODE_OPTIONS: {
|
|
||||||
key: PermissionMode;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
details: string[];
|
|
||||||
icon: string;
|
|
||||||
}[] = [
|
|
||||||
{
|
|
||||||
key: 'default',
|
|
||||||
label: 'デフォルト (default)',
|
|
||||||
description: 'Agent SDK標準モード(ファイル編集自動承認、最小限の確認)',
|
|
||||||
details: [
|
|
||||||
'Claude Agent SDKの標準設定(acceptEdits)を使用',
|
|
||||||
'ファイル編集は自動承認され、確認プロンプトなしで実行',
|
|
||||||
'Bash等の危険な操作は権限確認が表示される',
|
|
||||||
'通常の開発作業に推奨',
|
|
||||||
],
|
|
||||||
icon: '📋',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sacrifice-my-pc',
|
|
||||||
label: 'SACRIFICE-MY-PC',
|
|
||||||
description: '全ての権限リクエストが自動承認されます',
|
|
||||||
details: [
|
|
||||||
'⚠️ 警告: 全ての操作が確認なしで実行されます',
|
|
||||||
'Bash, ファイル削除, システム操作も自動承認',
|
|
||||||
'ブロック状態(判断待ち)も自動スキップ',
|
|
||||||
'完全自動化が必要な場合のみ使用してください',
|
|
||||||
],
|
|
||||||
icon: '💀',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function getPermissionModeOptions(currentMode: PermissionMode): {
|
|
||||||
label: string;
|
|
||||||
value: PermissionMode;
|
|
||||||
description: string;
|
|
||||||
details: string[];
|
|
||||||
}[] {
|
|
||||||
return PERMISSION_MODE_OPTIONS.map((opt) => ({
|
|
||||||
label: currentMode === opt.key
|
|
||||||
? (opt.key === 'sacrifice-my-pc' ? chalk.red : chalk.blue)(`${opt.icon} ${opt.label}`) + ' (current)'
|
|
||||||
: (opt.key === 'sacrifice-my-pc' ? chalk.red : chalk.blue)(`${opt.icon} ${opt.label}`),
|
|
||||||
value: opt.key,
|
|
||||||
description: opt.description,
|
|
||||||
details: opt.details,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current permission mode from project config
|
|
||||||
*/
|
|
||||||
export function getCurrentPermissionMode(cwd: string): PermissionMode {
|
|
||||||
const config = loadProjectConfig(cwd);
|
|
||||||
if (config.permissionMode) {
|
|
||||||
return config.permissionMode as PermissionMode;
|
|
||||||
}
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set permission mode in project config
|
|
||||||
*/
|
|
||||||
export function setPermissionMode(cwd: string, mode: PermissionMode): void {
|
|
||||||
updateProjectConfig(cwd, 'permissionMode', mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch permission mode (like switchPiece)
|
|
||||||
* @returns true if switch was successful
|
|
||||||
*/
|
|
||||||
export async function switchConfig(cwd: string, modeName?: string): Promise<boolean> {
|
|
||||||
const currentMode = getCurrentPermissionMode(cwd);
|
|
||||||
|
|
||||||
// No mode specified - show selection prompt
|
|
||||||
if (!modeName) {
|
|
||||||
info(`Current mode: ${currentMode}`);
|
|
||||||
|
|
||||||
const options = getPermissionModeOptions(currentMode);
|
|
||||||
const selected = await selectOption('Select permission mode:', options);
|
|
||||||
|
|
||||||
if (!selected) {
|
|
||||||
info('Cancelled');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
modeName = selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mode name
|
|
||||||
if (modeName !== 'default' && modeName !== 'sacrifice-my-pc') {
|
|
||||||
info(`Invalid mode: ${modeName}`);
|
|
||||||
info('Available modes: default, sacrifice-my-pc');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalMode: PermissionMode = modeName as PermissionMode;
|
|
||||||
|
|
||||||
// Save to project config
|
|
||||||
setPermissionMode(cwd, finalMode);
|
|
||||||
|
|
||||||
if (finalMode === 'sacrifice-my-pc') {
|
|
||||||
success('Switched to: sacrifice-my-pc 💀');
|
|
||||||
info('All permission requests will be auto-approved.');
|
|
||||||
} else {
|
|
||||||
success('Switched to: default 📋');
|
|
||||||
info('Using Agent SDK default mode (acceptEdits - minimal permission prompts).');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {
|
import {
|
||||||
loadGlobalConfig,
|
loadConfig,
|
||||||
loadPersonaSessions,
|
loadPersonaSessions,
|
||||||
updatePersonaSession,
|
updatePersonaSession,
|
||||||
loadSessionState,
|
loadSessionState,
|
||||||
@ -58,7 +58,7 @@ export interface SessionContext {
|
|||||||
* Initialize provider, session, and language for interactive conversation.
|
* Initialize provider, session, and language for interactive conversation.
|
||||||
*/
|
*/
|
||||||
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
if (!globalConfig.provider) {
|
if (!globalConfig.provider) {
|
||||||
throw new Error('Provider is not configured.');
|
throw new Error('Provider is not configured.');
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import { resolveLanguage } from './interactive.js';
|
import { resolveLanguage } from './interactive.js';
|
||||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||||
import { getLabelObject } from '../../shared/i18n/index.js';
|
import { getLabelObject } from '../../shared/i18n/index.js';
|
||||||
import { loadGlobalConfig } from '../../infra/config/index.js';
|
import { loadConfig } from '../../infra/config/index.js';
|
||||||
import type { InstructModeResult, InstructUIText } from '../tasks/list/instructMode.js';
|
import type { InstructModeResult, InstructUIText } from '../tasks/list/instructMode.js';
|
||||||
|
|
||||||
/** Failure information for a retry task */
|
/** Failure information for a retry task */
|
||||||
@ -116,7 +116,7 @@ export async function runRetryMode(
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
retryContext: RetryContext,
|
retryContext: RetryContext,
|
||||||
): Promise<InstructModeResult> {
|
): Promise<InstructModeResult> {
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
if (!globalConfig.provider) {
|
if (!globalConfig.provider) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
} from '../../infra/github/index.js';
|
} from '../../infra/github/index.js';
|
||||||
import { stageAndCommit, getCurrentBranch } from '../../infra/task/index.js';
|
import { stageAndCommit, getCurrentBranch } from '../../infra/task/index.js';
|
||||||
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
||||||
import { loadGlobalConfig } from '../../infra/config/index.js';
|
import { loadConfig } from '../../infra/config/index.js';
|
||||||
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import type { PipelineConfig } from '../../core/models/index.js';
|
import type { PipelineConfig } from '../../core/models/index.js';
|
||||||
@ -106,7 +106,7 @@ function buildPipelinePrBody(
|
|||||||
*/
|
*/
|
||||||
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
||||||
const { cwd, piece, autoPr, skipGit } = options;
|
const { cwd, piece, autoPr, skipGit } = options;
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
const pipelineConfig = globalConfig.pipeline;
|
const pipelineConfig = globalConfig.pipeline;
|
||||||
let issue: GitHubIssue | undefined;
|
let issue: GitHubIssue | undefined;
|
||||||
let task: string;
|
let task: string;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* Useful for debugging and understanding what prompts agents will receive.
|
* Useful for debugging and understanding what prompts agents will receive.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadPieceByIdentifier, getCurrentPiece, loadGlobalConfig } from '../../infra/config/index.js';
|
import { loadPieceByIdentifier, getCurrentPiece, loadConfig } from '../../infra/config/index.js';
|
||||||
import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js';
|
import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js';
|
||||||
import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js';
|
import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js';
|
||||||
import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js';
|
import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js';
|
||||||
@ -29,7 +29,7 @@ export async function previewPrompts(cwd: string, pieceIdentifier?: string): Pro
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
const language: Language = globalConfig.language ?? 'en';
|
const language: Language = globalConfig.language ?? 'en';
|
||||||
|
|
||||||
header(`Prompt Preview: ${config.name}`);
|
header(`Prompt Preview: ${config.name}`);
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
updatePersonaSession,
|
updatePersonaSession,
|
||||||
loadWorktreeSessions,
|
loadWorktreeSessions,
|
||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
loadGlobalConfig,
|
loadConfig,
|
||||||
saveSessionState,
|
saveSessionState,
|
||||||
type SessionState,
|
type SessionState,
|
||||||
} from '../../../infra/config/index.js';
|
} from '../../../infra/config/index.js';
|
||||||
@ -317,7 +317,7 @@ export async function executePiece(
|
|||||||
|
|
||||||
// Load saved agent sessions only on retry; normal runs start with empty sessions
|
// Load saved agent sessions only on retry; normal runs start with empty sessions
|
||||||
const isWorktree = cwd !== projectCwd;
|
const isWorktree = cwd !== projectCwd;
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(projectCwd);
|
||||||
const shouldNotify = globalConfig.notificationSound !== false;
|
const shouldNotify = globalConfig.notificationSound !== false;
|
||||||
const notificationSoundEvents = globalConfig.notificationSoundEvents;
|
const notificationSoundEvents = globalConfig.notificationSoundEvents;
|
||||||
const shouldNotifyIterationLimit = shouldNotify && notificationSoundEvents?.iterationLimit !== false;
|
const shouldNotifyIterationLimit = shouldNotify && notificationSoundEvents?.iterationLimit !== false;
|
||||||
@ -446,6 +446,8 @@ export async function executePiece(
|
|||||||
projectProvider: options.projectProvider,
|
projectProvider: options.projectProvider,
|
||||||
globalProvider: options.globalProvider,
|
globalProvider: options.globalProvider,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
|
projectProviderOptions: options.projectProviderOptions,
|
||||||
|
globalProviderOptions: options.globalProviderOptions,
|
||||||
personaProviders: options.personaProviders,
|
personaProviders: options.personaProviders,
|
||||||
projectProviderProfiles: options.projectProviderProfiles,
|
projectProviderProfiles: options.projectProviderProfiles,
|
||||||
globalProviderProfiles: options.globalProviderProfiles,
|
globalProviderProfiles: options.globalProviderProfiles,
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* instructBranch (instruct mode from takt list).
|
* instructBranch (instruct mode from takt list).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadGlobalConfig } from '../../../infra/config/index.js';
|
import { loadConfig } from '../../../infra/config/index.js';
|
||||||
import { confirm } from '../../../shared/prompt/index.js';
|
import { confirm } from '../../../shared/prompt/index.js';
|
||||||
import { autoCommitAndPush } from '../../../infra/task/index.js';
|
import { autoCommitAndPush } from '../../../infra/task/index.js';
|
||||||
import { info, error, success } from '../../../shared/ui/index.js';
|
import { info, error, success } from '../../../shared/ui/index.js';
|
||||||
@ -18,12 +18,12 @@ const log = createLogger('postExecution');
|
|||||||
/**
|
/**
|
||||||
* Resolve auto-PR setting with priority: CLI option > config > prompt.
|
* Resolve auto-PR setting with priority: CLI option > config > prompt.
|
||||||
*/
|
*/
|
||||||
export async function resolveAutoPr(optionAutoPr: boolean | undefined): Promise<boolean> {
|
export async function resolveAutoPr(optionAutoPr: boolean | undefined, cwd: string): Promise<boolean> {
|
||||||
if (typeof optionAutoPr === 'boolean') {
|
if (typeof optionAutoPr === 'boolean') {
|
||||||
return optionAutoPr;
|
return optionAutoPr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
if (typeof globalConfig.autoPr === 'boolean') {
|
if (typeof globalConfig.autoPr === 'boolean') {
|
||||||
return globalConfig.autoPr;
|
return globalConfig.autoPr;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { loadGlobalConfig } from '../../../infra/config/index.js';
|
import { loadConfig } from '../../../infra/config/index.js';
|
||||||
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
||||||
import { withProgress } from '../../../shared/ui/index.js';
|
import { withProgress } from '../../../shared/ui/index.js';
|
||||||
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
||||||
@ -141,7 +141,7 @@ export async function resolveTaskExecution(
|
|||||||
if (data.auto_pr !== undefined) {
|
if (data.auto_pr !== undefined) {
|
||||||
autoPr = data.auto_pr;
|
autoPr = data.auto_pr;
|
||||||
} else {
|
} else {
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(defaultCwd);
|
||||||
autoPr = globalConfig.autoPr ?? false;
|
autoPr = globalConfig.autoPr ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export async function selectAndExecuteTask(
|
|||||||
// Ask for PR creation BEFORE execution (only if worktree is enabled)
|
// Ask for PR creation BEFORE execution (only if worktree is enabled)
|
||||||
let shouldCreatePr = false;
|
let shouldCreatePr = false;
|
||||||
if (isWorktree) {
|
if (isWorktree) {
|
||||||
shouldCreatePr = await resolveAutoPr(options?.autoPr);
|
shouldCreatePr = await resolveAutoPr(options?.autoPr, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr });
|
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr });
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Session management helpers for agent execution
|
* Session management helpers for agent execution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadPersonaSessions, updatePersonaSession, loadGlobalConfig } from '../../../infra/config/index.js';
|
import { loadPersonaSessions, updatePersonaSession, loadConfig } from '../../../infra/config/index.js';
|
||||||
import type { AgentResponse } from '../../../core/models/index.js';
|
import type { AgentResponse } from '../../../core/models/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,7 +15,7 @@ export async function withPersonaSession(
|
|||||||
fn: (sessionId?: string) => Promise<AgentResponse>,
|
fn: (sessionId?: string) => Promise<AgentResponse>,
|
||||||
provider?: string
|
provider?: string
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const resolvedProvider = provider ?? loadGlobalConfig().provider ?? 'claude';
|
const resolvedProvider = provider ?? loadConfig(cwd).global.provider ?? 'claude';
|
||||||
const sessions = loadPersonaSessions(cwd, resolvedProvider);
|
const sessions = loadPersonaSessions(cwd, resolvedProvider);
|
||||||
const sessionId = sessions[personaName];
|
const sessionId = sessions[personaName];
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Task execution logic
|
* Task execution logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadPieceByIdentifier, isPiecePath, loadGlobalConfig, loadProjectConfig } from '../../../infra/config/index.js';
|
import { loadPieceByIdentifier, isPiecePath, loadConfig } from '../../../infra/config/index.js';
|
||||||
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
@ -86,8 +86,9 @@ async function executeTaskWithResult(options: ExecuteTaskOptions): Promise<Piece
|
|||||||
movements: pieceConfig.movements.map((s: { name: string }) => s.name),
|
movements: pieceConfig.movements.map((s: { name: string }) => s.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
const config = loadConfig(projectCwd);
|
||||||
const projectConfig = loadProjectConfig(projectCwd);
|
const globalConfig = config.global;
|
||||||
|
const projectConfig = config.project;
|
||||||
return await executePiece(pieceConfig, task, cwd, {
|
return await executePiece(pieceConfig, task, cwd, {
|
||||||
projectCwd,
|
projectCwd,
|
||||||
language: globalConfig.language,
|
language: globalConfig.language,
|
||||||
@ -95,6 +96,8 @@ async function executeTaskWithResult(options: ExecuteTaskOptions): Promise<Piece
|
|||||||
projectProvider: projectConfig.provider,
|
projectProvider: projectConfig.provider,
|
||||||
globalProvider: globalConfig.provider,
|
globalProvider: globalConfig.provider,
|
||||||
model: agentOverrides?.model,
|
model: agentOverrides?.model,
|
||||||
|
projectProviderOptions: projectConfig.providerOptions,
|
||||||
|
globalProviderOptions: globalConfig.providerOptions,
|
||||||
personaProviders: globalConfig.personaProviders,
|
personaProviders: globalConfig.personaProviders,
|
||||||
projectProviderProfiles: projectConfig.providerProfiles,
|
projectProviderProfiles: projectConfig.providerProfiles,
|
||||||
globalProviderProfiles: globalConfig.providerProfiles,
|
globalProviderProfiles: globalConfig.providerProfiles,
|
||||||
@ -234,7 +237,7 @@ export async function runAllTasks(
|
|||||||
options?: TaskExecutionOptions,
|
options?: TaskExecutionOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const taskRunner = new TaskRunner(cwd);
|
const taskRunner = new TaskRunner(cwd);
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
const shouldNotifyRunComplete = globalConfig.notificationSound !== false
|
const shouldNotifyRunComplete = globalConfig.notificationSound !== false
|
||||||
&& globalConfig.notificationSoundEvents?.runComplete !== false;
|
&& globalConfig.notificationSoundEvents?.runComplete !== false;
|
||||||
const shouldNotifyRunAbort = globalConfig.notificationSound !== false
|
const shouldNotifyRunAbort = globalConfig.notificationSound !== false
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import type { Language } from '../../../core/models/index.js';
|
import type { Language } from '../../../core/models/index.js';
|
||||||
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
||||||
|
import type { MovementProviderOptions } from '../../../core/models/piece-types.js';
|
||||||
import type { ProviderType } from '../../../infra/providers/index.js';
|
import type { ProviderType } from '../../../infra/providers/index.js';
|
||||||
import type { GitHubIssue } from '../../../infra/github/index.js';
|
import type { GitHubIssue } from '../../../infra/github/index.js';
|
||||||
|
|
||||||
@ -37,6 +38,10 @@ export interface PieceExecutionOptions {
|
|||||||
/** Global config provider */
|
/** Global config provider */
|
||||||
globalProvider?: ProviderType;
|
globalProvider?: ProviderType;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** Project-level provider options */
|
||||||
|
projectProviderOptions?: MovementProviderOptions;
|
||||||
|
/** Global-level provider options */
|
||||||
|
globalProviderOptions?: MovementProviderOptions;
|
||||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||||
personaProviders?: Record<string, ProviderType>;
|
personaProviders?: Record<string, ProviderType>;
|
||||||
/** Project-level provider permission profiles */
|
/** Project-level provider permission profiles */
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
||||||
import { loadTemplate } from '../../../shared/prompts/index.js';
|
import { loadTemplate } from '../../../shared/prompts/index.js';
|
||||||
import { getLabelObject } from '../../../shared/i18n/index.js';
|
import { getLabelObject } from '../../../shared/i18n/index.js';
|
||||||
import { loadGlobalConfig } from '../../../infra/config/index.js';
|
import { loadConfig } from '../../../infra/config/index.js';
|
||||||
|
|
||||||
export type InstructModeAction = 'execute' | 'save_task' | 'cancel';
|
export type InstructModeAction = 'execute' | 'save_task' | 'cancel';
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export async function runInstructMode(
|
|||||||
pieceContext?: PieceContext,
|
pieceContext?: PieceContext,
|
||||||
runSessionContext?: RunSessionContext,
|
runSessionContext?: RunSessionContext,
|
||||||
): Promise<InstructModeResult> {
|
): Promise<InstructModeResult> {
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(cwd);
|
||||||
const lang = resolveLanguage(globalConfig.language);
|
const lang = resolveLanguage(globalConfig.language);
|
||||||
|
|
||||||
if (!globalConfig.provider) {
|
if (!globalConfig.provider) {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
TaskRunner,
|
TaskRunner,
|
||||||
detectDefaultBranch,
|
detectDefaultBranch,
|
||||||
} from '../../../infra/task/index.js';
|
} from '../../../infra/task/index.js';
|
||||||
import { loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
|
import { loadConfig, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
import { info, error as logError } from '../../../shared/ui/index.js';
|
import { info, error as logError } from '../../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { runInstructMode } from './instructMode.js';
|
import { runInstructMode } from './instructMode.js';
|
||||||
@ -93,7 +93,7 @@ export async function instructBranch(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(projectDir);
|
||||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
||||||
const pieceContext: PieceContext = {
|
const pieceContext: PieceContext = {
|
||||||
name: pieceDesc.name,
|
name: pieceDesc.name,
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import type { TaskListItem } from '../../../infra/task/index.js';
|
import type { TaskListItem } from '../../../infra/task/index.js';
|
||||||
import { TaskRunner } from '../../../infra/task/index.js';
|
import { TaskRunner } from '../../../infra/task/index.js';
|
||||||
import { loadPieceByIdentifier, loadGlobalConfig, getPieceDescription } from '../../../infra/config/index.js';
|
import { loadPieceByIdentifier, loadConfig, getPieceDescription } from '../../../infra/config/index.js';
|
||||||
import { selectPiece } from '../../pieceSelection/index.js';
|
import { selectPiece } from '../../pieceSelection/index.js';
|
||||||
import { selectOption } from '../../../shared/prompt/index.js';
|
import { selectOption } from '../../../shared/prompt/index.js';
|
||||||
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
||||||
@ -133,7 +133,7 @@ export async function retryFailedTask(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(projectDir);
|
||||||
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
||||||
|
|
||||||
if (!pieceConfig) {
|
if (!pieceConfig) {
|
||||||
|
|||||||
@ -30,7 +30,9 @@ export {
|
|||||||
} from './infra/config/loaders/index.js';
|
} from './infra/config/loaders/index.js';
|
||||||
export type { PieceSource, PieceWithSource, PieceDirEntry } from './infra/config/loaders/index.js';
|
export type { PieceSource, PieceWithSource, PieceDirEntry } from './infra/config/loaders/index.js';
|
||||||
export {
|
export {
|
||||||
loadProjectConfig,
|
loadConfig,
|
||||||
|
} from './infra/config/loadConfig.js';
|
||||||
|
export {
|
||||||
saveProjectConfig,
|
saveProjectConfig,
|
||||||
updateProjectConfig,
|
updateProjectConfig,
|
||||||
getCurrentPiece,
|
getCurrentPiece,
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export interface ClaudeCallOptions {
|
|||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
/** Custom handler for AskUserQuestion tool */
|
/** Custom handler for AskUserQuestion tool */
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks */
|
||||||
bypassPermissions?: boolean;
|
bypassPermissions?: boolean;
|
||||||
/** Anthropic API key to inject via env (bypasses CLI auth) */
|
/** Anthropic API key to inject via env (bypasses CLI auth) */
|
||||||
anthropicApiKey?: string;
|
anthropicApiKey?: string;
|
||||||
@ -172,7 +172,7 @@ export interface ClaudeSpawnOptions {
|
|||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
/** Custom handler for AskUserQuestion tool */
|
/** Custom handler for AskUserQuestion tool */
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks */
|
||||||
bypassPermissions?: boolean;
|
bypassPermissions?: boolean;
|
||||||
/** Anthropic API key to inject via env (bypasses CLI auth) */
|
/** Anthropic API key to inject via env (bypasses CLI auth) */
|
||||||
anthropicApiKey?: string;
|
anthropicApiKey?: string;
|
||||||
|
|||||||
142
src/infra/config/env/config-env-overrides.ts
vendored
Normal file
142
src/infra/config/env/config-env-overrides.ts
vendored
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
type EnvValueType = 'string' | 'boolean' | 'number' | 'json';
|
||||||
|
|
||||||
|
interface EnvSpec {
|
||||||
|
path: string;
|
||||||
|
type: EnvValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEnvSegment(segment: string): string {
|
||||||
|
return segment
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^_|_$/g, '')
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function envVarNameFromPath(path: string): string {
|
||||||
|
const key = path
|
||||||
|
.split('.')
|
||||||
|
.map(normalizeEnvSegment)
|
||||||
|
.filter((segment) => segment.length > 0)
|
||||||
|
.join('_');
|
||||||
|
return `TAKT_${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnvValue(envKey: string, raw: string, type: EnvValueType): unknown {
|
||||||
|
if (type === 'string') {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (type === 'boolean') {
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === 'true') return true;
|
||||||
|
if (normalized === 'false') return false;
|
||||||
|
throw new Error(`${envKey} must be one of: true, false`);
|
||||||
|
}
|
||||||
|
if (type === 'number') {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
const value = Number(trimmed);
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
throw new Error(`${envKey} must be a number`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${envKey} must be valid JSON`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNested(target: Record<string, unknown>, path: string, value: unknown): void {
|
||||||
|
const parts = path.split('.');
|
||||||
|
let current: Record<string, unknown> = target;
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!part) continue;
|
||||||
|
const next = current[part];
|
||||||
|
if (typeof next !== 'object' || next === null || Array.isArray(next)) {
|
||||||
|
current[part] = {};
|
||||||
|
}
|
||||||
|
current = current[part] as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
const leaf = parts[parts.length - 1];
|
||||||
|
if (!leaf) return;
|
||||||
|
current[leaf] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEnvOverrides(target: Record<string, unknown>, specs: readonly EnvSpec[]): void {
|
||||||
|
for (const spec of specs) {
|
||||||
|
const envKey = envVarNameFromPath(spec.path);
|
||||||
|
const raw = process.env[envKey];
|
||||||
|
if (raw === undefined) continue;
|
||||||
|
const parsedValue = parseEnvValue(envKey, raw, spec.type);
|
||||||
|
setNested(target, spec.path, parsedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLOBAL_ENV_SPECS: readonly EnvSpec[] = [
|
||||||
|
{ path: 'language', type: 'string' },
|
||||||
|
{ path: 'log_level', type: 'string' },
|
||||||
|
{ path: 'provider', type: 'string' },
|
||||||
|
{ path: 'model', type: 'string' },
|
||||||
|
{ path: 'observability', type: 'json' },
|
||||||
|
{ path: 'observability.provider_events', type: 'boolean' },
|
||||||
|
{ path: 'worktree_dir', type: 'string' },
|
||||||
|
{ path: 'auto_pr', type: 'boolean' },
|
||||||
|
{ path: 'disabled_builtins', type: 'json' },
|
||||||
|
{ path: 'enable_builtin_pieces', type: 'boolean' },
|
||||||
|
{ path: 'anthropic_api_key', type: 'string' },
|
||||||
|
{ path: 'openai_api_key', type: 'string' },
|
||||||
|
{ path: 'codex_cli_path', type: 'string' },
|
||||||
|
{ path: 'opencode_api_key', type: 'string' },
|
||||||
|
{ path: 'pipeline', type: 'json' },
|
||||||
|
{ path: 'pipeline.default_branch_prefix', type: 'string' },
|
||||||
|
{ path: 'pipeline.commit_message_template', type: 'string' },
|
||||||
|
{ path: 'pipeline.pr_body_template', type: 'string' },
|
||||||
|
{ path: 'minimal_output', type: 'boolean' },
|
||||||
|
{ path: 'bookmarks_file', type: 'string' },
|
||||||
|
{ path: 'piece_categories_file', type: 'string' },
|
||||||
|
{ path: 'persona_providers', type: 'json' },
|
||||||
|
{ path: 'provider_options', type: 'json' },
|
||||||
|
{ path: 'provider_options.codex.network_access', type: 'boolean' },
|
||||||
|
{ path: 'provider_options.opencode.network_access', type: 'boolean' },
|
||||||
|
{ path: 'provider_options.claude.sandbox.allow_unsandboxed_commands', type: 'boolean' },
|
||||||
|
{ path: 'provider_options.claude.sandbox.excluded_commands', type: 'json' },
|
||||||
|
{ path: 'provider_profiles', type: 'json' },
|
||||||
|
{ path: 'runtime', type: 'json' },
|
||||||
|
{ path: 'runtime.prepare', type: 'json' },
|
||||||
|
{ path: 'branch_name_strategy', type: 'string' },
|
||||||
|
{ path: 'prevent_sleep', type: 'boolean' },
|
||||||
|
{ path: 'notification_sound', type: 'boolean' },
|
||||||
|
{ path: 'notification_sound_events', type: 'json' },
|
||||||
|
{ path: 'notification_sound_events.iteration_limit', type: 'boolean' },
|
||||||
|
{ path: 'notification_sound_events.piece_complete', type: 'boolean' },
|
||||||
|
{ path: 'notification_sound_events.piece_abort', type: 'boolean' },
|
||||||
|
{ path: 'notification_sound_events.run_complete', type: 'boolean' },
|
||||||
|
{ path: 'notification_sound_events.run_abort', type: 'boolean' },
|
||||||
|
{ path: 'interactive_preview_movements', type: 'number' },
|
||||||
|
{ path: 'verbose', type: 'boolean' },
|
||||||
|
{ path: 'concurrency', type: 'number' },
|
||||||
|
{ path: 'task_poll_interval_ms', type: 'number' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROJECT_ENV_SPECS: readonly EnvSpec[] = [
|
||||||
|
{ path: 'piece', type: 'string' },
|
||||||
|
{ path: 'provider', type: 'string' },
|
||||||
|
{ path: 'verbose', type: 'boolean' },
|
||||||
|
{ path: 'provider_options', type: 'json' },
|
||||||
|
{ path: 'provider_options.codex.network_access', type: 'boolean' },
|
||||||
|
{ path: 'provider_options.opencode.network_access', type: 'boolean' },
|
||||||
|
{ path: 'provider_options.claude.sandbox.allow_unsandboxed_commands', type: 'boolean' },
|
||||||
|
{ path: 'provider_options.claude.sandbox.excluded_commands', type: 'json' },
|
||||||
|
{ path: 'provider_profiles', type: 'json' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function applyGlobalConfigEnvOverrides(target: Record<string, unknown>): void {
|
||||||
|
applyEnvOverrides(target, GLOBAL_ENV_SPECS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyProjectConfigEnvOverrides(target: Record<string, unknown>): void {
|
||||||
|
applyEnvOverrides(target, PROJECT_ENV_SPECS);
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import { normalizeProviderOptions } from '../loaders/pieceParser.js';
|
|||||||
import { getGlobalConfigPath } from '../paths.js';
|
import { getGlobalConfigPath } from '../paths.js';
|
||||||
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
||||||
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
||||||
|
import { applyGlobalConfigEnvOverrides, envVarNameFromPath } from '../env/config-env-overrides.js';
|
||||||
|
|
||||||
/** Claude-specific model aliases that are not valid for other providers */
|
/** Claude-specific model aliases that are not valid for other providers */
|
||||||
const CLAUDE_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku']);
|
const CLAUDE_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku']);
|
||||||
@ -107,20 +108,6 @@ function denormalizeProviderProfiles(
|
|||||||
}])) as Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
|
}])) as Record<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create default global configuration (fresh instance each call) */
|
|
||||||
function createDefaultGlobalConfig(): GlobalConfig {
|
|
||||||
return {
|
|
||||||
language: DEFAULT_LANGUAGE,
|
|
||||||
defaultPiece: 'default',
|
|
||||||
logLevel: 'info',
|
|
||||||
provider: 'claude',
|
|
||||||
enableBuiltinPieces: true,
|
|
||||||
interactivePreviewMovements: 3,
|
|
||||||
concurrency: 1,
|
|
||||||
taskPollIntervalMs: 500,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages global configuration loading and caching.
|
* Manages global configuration loading and caching.
|
||||||
* Singleton — use GlobalConfigManager.getInstance().
|
* Singleton — use GlobalConfigManager.getInstance().
|
||||||
@ -154,17 +141,23 @@ export class GlobalConfigManager {
|
|||||||
return this.cachedConfig;
|
return this.cachedConfig;
|
||||||
}
|
}
|
||||||
const configPath = getGlobalConfigPath();
|
const configPath = getGlobalConfigPath();
|
||||||
if (!existsSync(configPath)) {
|
|
||||||
const defaultConfig = createDefaultGlobalConfig();
|
const rawConfig: Record<string, unknown> = {};
|
||||||
this.cachedConfig = defaultConfig;
|
if (existsSync(configPath)) {
|
||||||
return defaultConfig;
|
|
||||||
}
|
|
||||||
const content = readFileSync(configPath, 'utf-8');
|
const content = readFileSync(configPath, 'utf-8');
|
||||||
const raw = parseYaml(content);
|
const parsedRaw = parseYaml(content);
|
||||||
const parsed = GlobalConfigSchema.parse(raw);
|
if (parsedRaw && typeof parsedRaw === 'object' && !Array.isArray(parsedRaw)) {
|
||||||
|
Object.assign(rawConfig, parsedRaw as Record<string, unknown>);
|
||||||
|
} else if (parsedRaw != null) {
|
||||||
|
throw new Error('Configuration error: ~/.takt/config.yaml must be a YAML object.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyGlobalConfigEnvOverrides(rawConfig);
|
||||||
|
|
||||||
|
const parsed = GlobalConfigSchema.parse(rawConfig);
|
||||||
const config: GlobalConfig = {
|
const config: GlobalConfig = {
|
||||||
language: parsed.language,
|
language: parsed.language,
|
||||||
defaultPiece: parsed.default_piece,
|
|
||||||
logLevel: parsed.log_level,
|
logLevel: parsed.log_level,
|
||||||
provider: parsed.provider,
|
provider: parsed.provider,
|
||||||
model: parsed.model,
|
model: parsed.model,
|
||||||
@ -204,6 +197,7 @@ export class GlobalConfigManager {
|
|||||||
runAbort: parsed.notification_sound_events.run_abort,
|
runAbort: parsed.notification_sound_events.run_abort,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
interactivePreviewMovements: parsed.interactive_preview_movements,
|
interactivePreviewMovements: parsed.interactive_preview_movements,
|
||||||
|
verbose: parsed.verbose,
|
||||||
concurrency: parsed.concurrency,
|
concurrency: parsed.concurrency,
|
||||||
taskPollIntervalMs: parsed.task_poll_interval_ms,
|
taskPollIntervalMs: parsed.task_poll_interval_ms,
|
||||||
};
|
};
|
||||||
@ -217,7 +211,6 @@ export class GlobalConfigManager {
|
|||||||
const configPath = getGlobalConfigPath();
|
const configPath = getGlobalConfigPath();
|
||||||
const raw: Record<string, unknown> = {
|
const raw: Record<string, unknown> = {
|
||||||
language: config.language,
|
language: config.language,
|
||||||
default_piece: config.defaultPiece,
|
|
||||||
log_level: config.logLevel,
|
log_level: config.logLevel,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
};
|
};
|
||||||
@ -316,6 +309,9 @@ export class GlobalConfigManager {
|
|||||||
if (config.interactivePreviewMovements !== undefined) {
|
if (config.interactivePreviewMovements !== undefined) {
|
||||||
raw.interactive_preview_movements = config.interactivePreviewMovements;
|
raw.interactive_preview_movements = config.interactivePreviewMovements;
|
||||||
}
|
}
|
||||||
|
if (config.verbose !== undefined) {
|
||||||
|
raw.verbose = config.verbose;
|
||||||
|
}
|
||||||
if (config.concurrency !== undefined && config.concurrency > 1) {
|
if (config.concurrency !== undefined && config.concurrency > 1) {
|
||||||
raw.concurrency = config.concurrency;
|
raw.concurrency = config.concurrency;
|
||||||
}
|
}
|
||||||
@ -383,7 +379,7 @@ export function setProvider(provider: 'claude' | 'codex' | 'opencode'): void {
|
|||||||
* Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
* Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
||||||
*/
|
*/
|
||||||
export function resolveAnthropicApiKey(): string | undefined {
|
export function resolveAnthropicApiKey(): string | undefined {
|
||||||
const envKey = process.env['TAKT_ANTHROPIC_API_KEY'];
|
const envKey = process.env[envVarNameFromPath('anthropic_api_key')];
|
||||||
if (envKey) return envKey;
|
if (envKey) return envKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -399,7 +395,7 @@ export function resolveAnthropicApiKey(): string | undefined {
|
|||||||
* Priority: TAKT_OPENAI_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
* Priority: TAKT_OPENAI_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
||||||
*/
|
*/
|
||||||
export function resolveOpenaiApiKey(): string | undefined {
|
export function resolveOpenaiApiKey(): string | undefined {
|
||||||
const envKey = process.env['TAKT_OPENAI_API_KEY'];
|
const envKey = process.env[envVarNameFromPath('openai_api_key')];
|
||||||
if (envKey) return envKey;
|
if (envKey) return envKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -415,7 +411,7 @@ export function resolveOpenaiApiKey(): string | undefined {
|
|||||||
* Priority: TAKT_CODEX_CLI_PATH env var > config.yaml > undefined (SDK vendored binary fallback)
|
* Priority: TAKT_CODEX_CLI_PATH env var > config.yaml > undefined (SDK vendored binary fallback)
|
||||||
*/
|
*/
|
||||||
export function resolveCodexCliPath(): string | undefined {
|
export function resolveCodexCliPath(): string | undefined {
|
||||||
const envPath = process.env['TAKT_CODEX_CLI_PATH'];
|
const envPath = process.env[envVarNameFromPath('codex_cli_path')];
|
||||||
if (envPath !== undefined) {
|
if (envPath !== undefined) {
|
||||||
return validateCodexCliPath(envPath, 'TAKT_CODEX_CLI_PATH');
|
return validateCodexCliPath(envPath, 'TAKT_CODEX_CLI_PATH');
|
||||||
}
|
}
|
||||||
@ -437,7 +433,7 @@ export function resolveCodexCliPath(): string | undefined {
|
|||||||
* Priority: TAKT_OPENCODE_API_KEY env var > config.yaml > undefined
|
* Priority: TAKT_OPENCODE_API_KEY env var > config.yaml > undefined
|
||||||
*/
|
*/
|
||||||
export function resolveOpencodeApiKey(): string | undefined {
|
export function resolveOpencodeApiKey(): string | undefined {
|
||||||
const envKey = process.env['TAKT_OPENCODE_API_KEY'];
|
const envKey = process.env[envVarNameFromPath('opencode_api_key')];
|
||||||
if (envKey) return envKey;
|
if (envKey) return envKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -447,4 +443,3 @@ export function resolveOpencodeApiKey(): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export * from './paths.js';
|
|||||||
export * from './loaders/index.js';
|
export * from './loaders/index.js';
|
||||||
export * from './global/index.js';
|
export * from './global/index.js';
|
||||||
export * from './project/index.js';
|
export * from './project/index.js';
|
||||||
|
export * from './loadConfig.js';
|
||||||
|
|||||||
16
src/infra/config/loadConfig.ts
Normal file
16
src/infra/config/loadConfig.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { GlobalConfig } from '../../core/models/index.js';
|
||||||
|
import type { ProjectLocalConfig } from './project/projectConfig.js';
|
||||||
|
import { loadGlobalConfig } from './global/globalConfig.js';
|
||||||
|
import { loadProjectConfig } from './project/projectConfig.js';
|
||||||
|
|
||||||
|
export interface LoadedConfig {
|
||||||
|
global: GlobalConfig;
|
||||||
|
project: ProjectLocalConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(projectDir: string): LoadedConfig {
|
||||||
|
return {
|
||||||
|
global: loadGlobalConfig(),
|
||||||
|
project: loadProjectConfig(projectDir),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -119,9 +119,11 @@ export {
|
|||||||
updateProjectConfig,
|
updateProjectConfig,
|
||||||
getCurrentPiece,
|
getCurrentPiece,
|
||||||
setCurrentPiece,
|
setCurrentPiece,
|
||||||
isVerboseMode,
|
|
||||||
type ProjectLocalConfig,
|
type ProjectLocalConfig,
|
||||||
} from './project/projectConfig.js';
|
} from './project/projectConfig.js';
|
||||||
|
export {
|
||||||
|
isVerboseMode,
|
||||||
|
} from './project/resolvedSettings.js';
|
||||||
|
|
||||||
// Re-export session storage functions
|
// Re-export session storage functions
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -8,10 +8,11 @@ export {
|
|||||||
updateProjectConfig,
|
updateProjectConfig,
|
||||||
getCurrentPiece,
|
getCurrentPiece,
|
||||||
setCurrentPiece,
|
setCurrentPiece,
|
||||||
isVerboseMode,
|
|
||||||
type PermissionMode,
|
|
||||||
type ProjectLocalConfig,
|
type ProjectLocalConfig,
|
||||||
} from './projectConfig.js';
|
} from './projectConfig.js';
|
||||||
|
export {
|
||||||
|
isVerboseMode,
|
||||||
|
} from './resolvedSettings.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
writeFileAtomic,
|
writeFileAtomic,
|
||||||
|
|||||||
@ -8,15 +8,16 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import { parse, stringify } from 'yaml';
|
import { parse, stringify } from 'yaml';
|
||||||
import { copyProjectResourcesToDir } from '../../resources/index.js';
|
import { copyProjectResourcesToDir } from '../../resources/index.js';
|
||||||
import type { PermissionMode, ProjectLocalConfig } from '../types.js';
|
import type { ProjectLocalConfig } from '../types.js';
|
||||||
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
||||||
|
import { applyProjectConfigEnvOverrides } from '../env/config-env-overrides.js';
|
||||||
|
import { normalizeProviderOptions } from '../loaders/pieceParser.js';
|
||||||
|
|
||||||
export type { PermissionMode, ProjectLocalConfig };
|
export type { ProjectLocalConfig } from '../types.js';
|
||||||
|
|
||||||
/** Default project configuration */
|
/** Default project configuration */
|
||||||
const DEFAULT_PROJECT_CONFIG: ProjectLocalConfig = {
|
const DEFAULT_PROJECT_CONFIG: ProjectLocalConfig = {
|
||||||
piece: 'default',
|
piece: 'default',
|
||||||
permissionMode: 'default',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,23 +64,36 @@ function denormalizeProviderProfiles(profiles: ProviderPermissionProfiles | unde
|
|||||||
export function loadProjectConfig(projectDir: string): ProjectLocalConfig {
|
export function loadProjectConfig(projectDir: string): ProjectLocalConfig {
|
||||||
const configPath = getConfigPath(projectDir);
|
const configPath = getConfigPath(projectDir);
|
||||||
|
|
||||||
if (!existsSync(configPath)) {
|
const parsedConfig: Record<string, unknown> = {};
|
||||||
return { ...DEFAULT_PROJECT_CONFIG };
|
if (existsSync(configPath)) {
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(configPath, 'utf-8');
|
const content = readFileSync(configPath, 'utf-8');
|
||||||
const parsed = (parse(content) as ProjectLocalConfig | null) ?? {};
|
const parsed = (parse(content) as Record<string, unknown> | null) ?? {};
|
||||||
return {
|
Object.assign(parsedConfig, parsed);
|
||||||
...DEFAULT_PROJECT_CONFIG,
|
|
||||||
...parsed,
|
|
||||||
providerProfiles: normalizeProviderProfiles(parsed.provider_profiles as Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined),
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULT_PROJECT_CONFIG };
|
return { ...DEFAULT_PROJECT_CONFIG };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyProjectConfigEnvOverrides(parsedConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_PROJECT_CONFIG,
|
||||||
|
...(parsedConfig as ProjectLocalConfig),
|
||||||
|
providerOptions: normalizeProviderOptions(parsedConfig.provider_options as {
|
||||||
|
codex?: { network_access?: boolean };
|
||||||
|
opencode?: { network_access?: boolean };
|
||||||
|
claude?: {
|
||||||
|
sandbox?: {
|
||||||
|
allow_unsandboxed_commands?: boolean;
|
||||||
|
excluded_commands?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} | undefined),
|
||||||
|
providerProfiles: normalizeProviderProfiles(parsedConfig.provider_profiles as Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save project configuration to .takt/config.yaml
|
* Save project configuration to .takt/config.yaml
|
||||||
*/
|
*/
|
||||||
@ -103,6 +117,7 @@ export function saveProjectConfig(projectDir: string, config: ProjectLocalConfig
|
|||||||
delete savePayload.provider_profiles;
|
delete savePayload.provider_profiles;
|
||||||
}
|
}
|
||||||
delete savePayload.providerProfiles;
|
delete savePayload.providerProfiles;
|
||||||
|
delete savePayload.providerOptions;
|
||||||
|
|
||||||
const content = stringify(savePayload, { indent: 2 });
|
const content = stringify(savePayload, { indent: 2 });
|
||||||
writeFileSync(configPath, content, 'utf-8');
|
writeFileSync(configPath, content, 'utf-8');
|
||||||
@ -135,11 +150,3 @@ export function getCurrentPiece(projectDir: string): string {
|
|||||||
export function setCurrentPiece(projectDir: string, piece: string): void {
|
export function setCurrentPiece(projectDir: string, piece: string): void {
|
||||||
updateProjectConfig(projectDir, 'piece', piece);
|
updateProjectConfig(projectDir, 'piece', piece);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get verbose mode from project config
|
|
||||||
*/
|
|
||||||
export function isVerboseMode(projectDir: string): boolean {
|
|
||||||
const config = loadProjectConfig(projectDir);
|
|
||||||
return config.verbose === true;
|
|
||||||
}
|
|
||||||
|
|||||||
32
src/infra/config/project/resolvedSettings.ts
Normal file
32
src/infra/config/project/resolvedSettings.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { envVarNameFromPath } from '../env/config-env-overrides.js';
|
||||||
|
import { loadConfig } from '../loadConfig.js';
|
||||||
|
|
||||||
|
function resolveValue<T>(
|
||||||
|
envValue: T | undefined,
|
||||||
|
localValue: T | undefined,
|
||||||
|
globalValue: T | undefined,
|
||||||
|
defaultValue: T,
|
||||||
|
): T {
|
||||||
|
if (envValue !== undefined) return envValue;
|
||||||
|
if (localValue !== undefined) return localValue;
|
||||||
|
if (globalValue !== undefined) return globalValue;
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEnvBooleanSetting(configKey: string): boolean | undefined {
|
||||||
|
const envKey = envVarNameFromPath(configKey);
|
||||||
|
const raw = process.env[envKey];
|
||||||
|
if (raw === undefined) return undefined;
|
||||||
|
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === 'true') return true;
|
||||||
|
if (normalized === 'false') return false;
|
||||||
|
|
||||||
|
throw new Error(`${envKey} must be one of: true, false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVerboseMode(projectDir: string): boolean {
|
||||||
|
const envValue = loadEnvBooleanSetting('verbose');
|
||||||
|
const { project, global } = loadConfig(projectDir);
|
||||||
|
return resolveValue(envValue, project.verbose, global.verbose, false);
|
||||||
|
}
|
||||||
@ -2,40 +2,25 @@
|
|||||||
* Config module type definitions
|
* Config module type definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PieceCategoryConfigNode } from '../../core/models/schemas.js';
|
|
||||||
import type { MovementProviderOptions } from '../../core/models/piece-types.js';
|
import type { MovementProviderOptions } from '../../core/models/piece-types.js';
|
||||||
import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js';
|
import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js';
|
||||||
|
|
||||||
/** Permission mode for the project
|
|
||||||
* - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts)
|
|
||||||
* - sacrifice-my-pc: Auto-approves all permission requests (bypassPermissions)
|
|
||||||
*
|
|
||||||
* Note: 'confirm' mode is planned but not yet implemented
|
|
||||||
*/
|
|
||||||
export type PermissionMode = 'default' | 'sacrifice-my-pc';
|
|
||||||
|
|
||||||
/** Project configuration stored in .takt/config.yaml */
|
/** Project configuration stored in .takt/config.yaml */
|
||||||
export interface ProjectLocalConfig {
|
export interface ProjectLocalConfig {
|
||||||
/** Current piece name */
|
/** Current piece name */
|
||||||
piece?: string;
|
piece?: string;
|
||||||
/** Provider selection for agent runtime */
|
/** Provider selection for agent runtime */
|
||||||
provider?: 'claude' | 'codex' | 'opencode';
|
provider?: 'claude' | 'codex' | 'opencode';
|
||||||
/** Permission mode setting */
|
|
||||||
permissionMode?: PermissionMode;
|
|
||||||
/** Verbose output mode */
|
/** Verbose output mode */
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
/** Provider-specific options (overrides global, overridden by piece/movement) */
|
/** Provider-specific options (overrides global, overridden by piece/movement) */
|
||||||
provider_options?: MovementProviderOptions;
|
provider_options?: MovementProviderOptions;
|
||||||
|
/** Provider-specific options (camelCase alias) */
|
||||||
|
providerOptions?: MovementProviderOptions;
|
||||||
/** Provider-specific permission profiles (project-level override) */
|
/** Provider-specific permission profiles (project-level override) */
|
||||||
provider_profiles?: ProviderPermissionProfiles;
|
provider_profiles?: ProviderPermissionProfiles;
|
||||||
/** Provider-specific permission profiles (camelCase alias) */
|
/** Provider-specific permission profiles (camelCase alias) */
|
||||||
providerProfiles?: ProviderPermissionProfiles;
|
providerProfiles?: ProviderPermissionProfiles;
|
||||||
/** Piece categories (name -> piece list) */
|
|
||||||
piece_categories?: Record<string, PieceCategoryConfigNode>;
|
|
||||||
/** Show uncategorized pieces under Others category */
|
|
||||||
show_others_category?: boolean;
|
|
||||||
/** Display name for Others category */
|
|
||||||
others_category_name?: string;
|
|
||||||
/** Custom settings */
|
/** Custom settings */
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import * as fs from 'node:fs';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createLogger, slugify } from '../../shared/utils/index.js';
|
import { createLogger, slugify } from '../../shared/utils/index.js';
|
||||||
import { loadGlobalConfig } from '../config/global/globalConfig.js';
|
import { loadConfig } from '../config/index.js';
|
||||||
import type { WorktreeOptions, WorktreeResult } from './types.js';
|
import type { WorktreeOptions, WorktreeResult } from './types.js';
|
||||||
|
|
||||||
export type { WorktreeOptions, WorktreeResult };
|
export type { WorktreeOptions, WorktreeResult };
|
||||||
@ -36,7 +36,7 @@ export class CloneManager {
|
|||||||
* Returns the configured worktree_dir (resolved to absolute), or ../
|
* Returns the configured worktree_dir (resolved to absolute), or ../
|
||||||
*/
|
*/
|
||||||
private static resolveCloneBaseDir(projectDir: string): string {
|
private static resolveCloneBaseDir(projectDir: string): string {
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(projectDir);
|
||||||
if (globalConfig.worktreeDir) {
|
if (globalConfig.worktreeDir) {
|
||||||
return path.isAbsolute(globalConfig.worktreeDir)
|
return path.isAbsolute(globalConfig.worktreeDir)
|
||||||
? globalConfig.worktreeDir
|
? globalConfig.worktreeDir
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as wanakana from 'wanakana';
|
import * as wanakana from 'wanakana';
|
||||||
import { loadGlobalConfig } from '../config/global/globalConfig.js';
|
import { loadConfig } from '../config/index.js';
|
||||||
import { getProvider, type ProviderType } from '../providers/index.js';
|
import { getProvider, type ProviderType } from '../providers/index.js';
|
||||||
import { createLogger } from '../../shared/utils/index.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||||
@ -53,7 +53,7 @@ export class TaskSummarizer {
|
|||||||
taskName: string,
|
taskName: string,
|
||||||
options: SummarizeOptions,
|
options: SummarizeOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const globalConfig = loadGlobalConfig();
|
const { global: globalConfig } = loadConfig(options.cwd);
|
||||||
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
|
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
|
||||||
log.info('Summarizing task name', { taskName, useLLM });
|
log.info('Summarizing task name', { taskName, useLLM });
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user