refactor: 設定参照をresolveConfigValueへ統一
This commit is contained in:
parent
5dc79946f2
commit
cbde7ac654
@ -17,8 +17,17 @@ import {
|
||||
|
||||
// Mock external dependencies to isolate unit tests
|
||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
getLanguage: () => 'en',
|
||||
getBuiltinPiecesEnabled: () => true,
|
||||
loadGlobalConfig: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||
loadConfig: () => ({
|
||||
global: {
|
||||
language: 'en',
|
||||
enableBuiltinPieces: true,
|
||||
},
|
||||
project: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockLogError = vi.fn();
|
||||
|
||||
@ -76,7 +76,7 @@ vi.mock('../infra/task/index.js', () => ({
|
||||
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
|
||||
loadConfig: vi.fn(() => ({ global: { interactivePreviewMovements: 3 }, project: {} })),
|
||||
resolveConfigValues: vi.fn(() => ({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' })),
|
||||
}));
|
||||
|
||||
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 { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
||||
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
|
||||
import { loadConfig } from '../infra/config/index.js';
|
||||
import { resolveConfigValues } from '../infra/config/index.js';
|
||||
import { confirm } from '../shared/prompt/index.js';
|
||||
import { isDirectTask } from '../app/cli/helpers.js';
|
||||
import { executeDefaultAction } from '../app/cli/routing.js';
|
||||
@ -123,7 +123,7 @@ const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
|
||||
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
||||
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||
const mockSelectRecentSession = vi.mocked(selectRecentSession);
|
||||
const mockLoadConfig = vi.mocked(loadConfig);
|
||||
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
|
||||
const mockConfirm = vi.mocked(confirm);
|
||||
const mockIsDirectTask = vi.mocked(isDirectTask);
|
||||
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
||||
@ -483,7 +483,7 @@ describe('Issue resolution in routing', () => {
|
||||
describe('session selection with provider=claude', () => {
|
||||
it('should pass selected session ID to interactiveMode when provider is claude', async () => {
|
||||
// Given
|
||||
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'claude' }, project: {} });
|
||||
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
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 () => {
|
||||
// Given
|
||||
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'claude' }, project: {} });
|
||||
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' });
|
||||
mockConfirm.mockResolvedValue(false);
|
||||
|
||||
// When
|
||||
@ -525,7 +525,7 @@ describe('Issue resolution in routing', () => {
|
||||
|
||||
it('should not call selectRecentSession when provider is not claude', async () => {
|
||||
// Given
|
||||
mockLoadConfig.mockReturnValue({ global: { interactivePreviewMovements: 3, provider: 'openai' }, project: {} });
|
||||
mockResolveConfigValues.mockReturnValue({ language: 'en', interactivePreviewMovements: 3, provider: 'openai' });
|
||||
|
||||
// When
|
||||
await executeDefaultAction();
|
||||
|
||||
@ -41,13 +41,13 @@ import {
|
||||
|
||||
describe('getBuiltinPiece', () => {
|
||||
it('should return builtin piece when it exists in resources', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
expect(piece).not.toBeNull();
|
||||
expect(piece!.name).toBe('default');
|
||||
});
|
||||
|
||||
it('should resolve builtin instruction_template without projectCwd', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
expect(piece).not.toBeNull();
|
||||
|
||||
const planMovement = piece!.movements.find((movement) => movement.name === 'plan');
|
||||
@ -56,15 +56,15 @@ describe('getBuiltinPiece', () => {
|
||||
});
|
||||
|
||||
it('should return null for non-existent piece names', () => {
|
||||
expect(getBuiltinPiece('nonexistent-piece')).toBeNull();
|
||||
expect(getBuiltinPiece('unknown')).toBeNull();
|
||||
expect(getBuiltinPiece('')).toBeNull();
|
||||
expect(getBuiltinPiece('nonexistent-piece', process.cwd())).toBeNull();
|
||||
expect(getBuiltinPiece('unknown', process.cwd())).toBeNull();
|
||||
expect(getBuiltinPiece('', process.cwd())).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('default piece parallel reviewers movement', () => {
|
||||
it('should have a reviewers movement with parallel sub-movements', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
expect(piece).not.toBeNull();
|
||||
|
||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers');
|
||||
@ -74,7 +74,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have arch-review and qa-review as parallel sub-movements', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||
const subMovementNames = reviewersMovement.parallel!.map((s) => s.name);
|
||||
|
||||
@ -83,7 +83,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have aggregate conditions on the reviewers parent movement', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||
|
||||
expect(reviewersMovement.rules).toBeDefined();
|
||||
@ -101,7 +101,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have matching conditions on sub-movements for aggregation', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||
|
||||
for (const subMovement of reviewersMovement.parallel!) {
|
||||
@ -113,7 +113,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have ai_review transitioning to reviewers movement', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const aiReviewMovement = piece!.movements.find((s) => s.name === 'ai_review')!;
|
||||
|
||||
const approveRule = aiReviewMovement.rules!.find((r) => r.next === 'reviewers');
|
||||
@ -121,7 +121,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have ai_fix transitioning to ai_review movement', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const aiFixMovement = piece!.movements.find((s) => s.name === 'ai_fix')!;
|
||||
|
||||
const fixedRule = aiFixMovement.rules!.find((r) => r.next === 'ai_review');
|
||||
@ -129,7 +129,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have fix movement transitioning back to reviewers', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const fixMovement = piece!.movements.find((s) => s.name === 'fix')!;
|
||||
|
||||
const fixedRule = fixMovement.rules!.find((r) => r.next === 'reviewers');
|
||||
@ -137,7 +137,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should not have old separate review/security_review/improve movements', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const movementNames = piece!.movements.map((s) => s.name);
|
||||
|
||||
expect(movementNames).not.toContain('review');
|
||||
@ -147,7 +147,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have sub-movements with correct agents', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||
|
||||
const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!;
|
||||
@ -158,7 +158,7 @@ describe('default piece parallel reviewers movement', () => {
|
||||
});
|
||||
|
||||
it('should have output contracts configured on sub-movements', () => {
|
||||
const piece = getBuiltinPiece('default');
|
||||
const piece = getBuiltinPiece('default', process.cwd());
|
||||
const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!;
|
||||
|
||||
const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!;
|
||||
@ -290,7 +290,7 @@ describe('loadPersonaPromptFromPath (builtin paths)', () => {
|
||||
const personaPath = join(builtinPersonasDir, 'coder.md');
|
||||
|
||||
if (existsSync(personaPath)) {
|
||||
const prompt = loadPersonaPromptFromPath(personaPath);
|
||||
const prompt = loadPersonaPromptFromPath(personaPath, process.cwd());
|
||||
expect(prompt).toBeTruthy();
|
||||
expect(typeof prompt).toBe('string');
|
||||
}
|
||||
|
||||
@ -78,12 +78,9 @@ describe('PieceEngine provider_options resolution', () => {
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||
projectCwd: tmpDir,
|
||||
provider: 'claude',
|
||||
globalProviderOptions: {
|
||||
providerOptions: {
|
||||
codex: { networkAccess: true },
|
||||
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
||||
},
|
||||
projectProviderOptions: {
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
opencode: { networkAccess: true },
|
||||
},
|
||||
});
|
||||
@ -96,7 +93,7 @@ describe('PieceEngine provider_options resolution', () => {
|
||||
opencode: { networkAccess: true },
|
||||
claude: {
|
||||
sandbox: {
|
||||
allowUnsandboxedCommands: true,
|
||||
allowUnsandboxedCommands: false,
|
||||
excludedCommands: ['./gradlew'],
|
||||
},
|
||||
},
|
||||
@ -123,7 +120,7 @@ describe('PieceEngine provider_options resolution', () => {
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||
projectCwd: tmpDir,
|
||||
provider: 'claude',
|
||||
globalProviderOptions: {
|
||||
providerOptions: {
|
||||
codex: { networkAccess: true },
|
||||
},
|
||||
});
|
||||
|
||||
@ -7,14 +7,14 @@ import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const loadGlobalConfigMock = vi.hoisted(() => vi.fn());
|
||||
const loadConfigMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../infra/config/paths.js', () => ({
|
||||
getGlobalConfigDir: () => '/tmp/.takt',
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
loadGlobalConfig: loadGlobalConfigMock,
|
||||
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||
loadConfig: loadConfigMock,
|
||||
}));
|
||||
|
||||
const { getPieceCategoriesPath, resetPieceCategories } = await import(
|
||||
@ -28,17 +28,18 @@ function createTempCategoriesPath(): string {
|
||||
|
||||
describe('getPieceCategoriesPath', () => {
|
||||
beforeEach(() => {
|
||||
loadGlobalConfigMock.mockReset();
|
||||
loadConfigMock.mockReset();
|
||||
});
|
||||
|
||||
it('should return configured path when pieceCategoriesFile is set', () => {
|
||||
// Given
|
||||
loadGlobalConfigMock.mockReturnValue({
|
||||
pieceCategoriesFile: '/custom/piece-categories.yaml',
|
||||
loadConfigMock.mockReturnValue({
|
||||
global: { pieceCategoriesFile: '/custom/piece-categories.yaml' },
|
||||
project: {},
|
||||
});
|
||||
|
||||
// When
|
||||
const path = getPieceCategoriesPath();
|
||||
const path = getPieceCategoriesPath(process.cwd());
|
||||
|
||||
// Then
|
||||
expect(path).toBe('/custom/piece-categories.yaml');
|
||||
@ -46,10 +47,10 @@ describe('getPieceCategoriesPath', () => {
|
||||
|
||||
it('should return default path when pieceCategoriesFile is not set', () => {
|
||||
// Given
|
||||
loadGlobalConfigMock.mockReturnValue({});
|
||||
loadConfigMock.mockReturnValue({ global: {}, project: {} });
|
||||
|
||||
// When
|
||||
const path = getPieceCategoriesPath();
|
||||
const path = getPieceCategoriesPath(process.cwd());
|
||||
|
||||
// Then
|
||||
expect(path).toBe('/tmp/.takt/preferences/piece-categories.yaml');
|
||||
@ -57,12 +58,12 @@ describe('getPieceCategoriesPath', () => {
|
||||
|
||||
it('should rethrow when global config loading fails', () => {
|
||||
// Given
|
||||
loadGlobalConfigMock.mockImplementation(() => {
|
||||
loadConfigMock.mockImplementation(() => {
|
||||
throw new Error('invalid global config');
|
||||
});
|
||||
|
||||
// When / Then
|
||||
expect(() => getPieceCategoriesPath()).toThrow('invalid global config');
|
||||
expect(() => getPieceCategoriesPath(process.cwd())).toThrow('invalid global config');
|
||||
});
|
||||
});
|
||||
|
||||
@ -70,7 +71,7 @@ describe('resetPieceCategories', () => {
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
loadGlobalConfigMock.mockReset();
|
||||
loadConfigMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -84,12 +85,13 @@ describe('resetPieceCategories', () => {
|
||||
// Given
|
||||
const categoriesPath = createTempCategoriesPath();
|
||||
tempRoots.push(dirname(dirname(categoriesPath)));
|
||||
loadGlobalConfigMock.mockReturnValue({
|
||||
pieceCategoriesFile: categoriesPath,
|
||||
loadConfigMock.mockReturnValue({
|
||||
global: { pieceCategoriesFile: categoriesPath },
|
||||
project: {},
|
||||
});
|
||||
|
||||
// When
|
||||
resetPieceCategories();
|
||||
resetPieceCategories(process.cwd());
|
||||
|
||||
// Then
|
||||
expect(existsSync(dirname(categoriesPath))).toBe(true);
|
||||
@ -102,14 +104,15 @@ describe('resetPieceCategories', () => {
|
||||
const categoriesDir = dirname(categoriesPath);
|
||||
const tempRoot = dirname(categoriesDir);
|
||||
tempRoots.push(tempRoot);
|
||||
loadGlobalConfigMock.mockReturnValue({
|
||||
pieceCategoriesFile: categoriesPath,
|
||||
loadConfigMock.mockReturnValue({
|
||||
global: { pieceCategoriesFile: categoriesPath },
|
||||
project: {},
|
||||
});
|
||||
mkdirSync(categoriesDir, { recursive: true });
|
||||
writeFileSync(categoriesPath, 'piece_categories:\n old:\n - stale-piece\n', 'utf-8');
|
||||
|
||||
// When
|
||||
resetPieceCategories();
|
||||
resetPieceCategories(process.cwd());
|
||||
|
||||
// Then
|
||||
expect(readFileSync(categoriesPath, 'utf-8')).toBe('piece_categories: {}\n');
|
||||
|
||||
@ -118,6 +118,10 @@ vi.mock('../infra/config/index.js', () => ({
|
||||
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
||||
updateWorktreeSession: vi.fn(),
|
||||
loadGlobalConfig: mockLoadGlobalConfig,
|
||||
loadConfig: vi.fn().mockImplementation(() => ({
|
||||
global: mockLoadGlobalConfig(),
|
||||
project: {},
|
||||
})),
|
||||
saveSessionState: vi.fn(),
|
||||
ensureDir: vi.fn(),
|
||||
writeFileAtomic: vi.fn(),
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Tests the 3-tier piece resolution (project-local → user → builtin)
|
||||
* and YAML parsing including special rule syntax (ai(), all(), any()).
|
||||
*
|
||||
* Mocked: globalConfig (for language/builtins)
|
||||
* Mocked: loadConfig (for language/builtins)
|
||||
* Not mocked: loadPiece, parsePiece, rule parsing
|
||||
*/
|
||||
|
||||
@ -18,9 +18,17 @@ const languageState = vi.hoisted(() => ({ value: 'en' as 'en' | 'ja' }));
|
||||
|
||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
||||
getLanguage: vi.fn(() => languageState.value),
|
||||
getDisabledBuiltins: vi.fn().mockReturnValue([]),
|
||||
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||
loadConfig: vi.fn(() => ({
|
||||
global: {
|
||||
language: languageState.value,
|
||||
disabledBuiltins: [],
|
||||
enableBuiltinPieces: true,
|
||||
},
|
||||
project: {},
|
||||
})),
|
||||
}));
|
||||
|
||||
// --- Imports (after mocks) ---
|
||||
@ -38,6 +46,7 @@ function createTestDir(): string {
|
||||
|
||||
describe('Piece Loader IT: builtin piece loading', () => {
|
||||
let testDir: string;
|
||||
const builtinNames = listBuiltinPieceNames(process.cwd(), { includeDisabled: true });
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = createTestDir();
|
||||
@ -48,8 +57,6 @@ describe('Piece Loader IT: builtin piece loading', () => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const builtinNames = listBuiltinPieceNames({ includeDisabled: true });
|
||||
|
||||
for (const name of builtinNames) {
|
||||
it(`should load builtin piece: ${name}`, () => {
|
||||
const config = loadPiece(name, testDir);
|
||||
@ -85,7 +92,7 @@ describe('Piece Loader IT: builtin piece loading', () => {
|
||||
it('should load e2e-test as a builtin piece in ja locale', () => {
|
||||
languageState.value = 'ja';
|
||||
|
||||
const jaBuiltinNames = listBuiltinPieceNames({ includeDisabled: true });
|
||||
const jaBuiltinNames = listBuiltinPieceNames(testDir, { includeDisabled: true });
|
||||
expect(jaBuiltinNames).toContain('e2e-test');
|
||||
|
||||
const config = loadPiece('e2e-test', testDir);
|
||||
|
||||
@ -57,6 +57,17 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
|
||||
loadProjectConfig: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
global: {
|
||||
language: 'en',
|
||||
enableBuiltinPieces: true,
|
||||
disabledBuiltins: [],
|
||||
},
|
||||
project: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
import { PieceEngine } from '../core/piece/index.js';
|
||||
|
||||
@ -118,7 +118,11 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
|
||||
return {
|
||||
...original,
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({
|
||||
language: 'en',
|
||||
enableBuiltinPieces: true,
|
||||
disabledBuiltins: [],
|
||||
}),
|
||||
getLanguage: vi.fn().mockReturnValue('en'),
|
||||
getDisabledBuiltins: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
@ -100,7 +100,11 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
|
||||
return {
|
||||
...original,
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({
|
||||
language: 'en',
|
||||
enableBuiltinPieces: true,
|
||||
disabledBuiltins: [],
|
||||
}),
|
||||
getLanguage: vi.fn().mockReturnValue('en'),
|
||||
};
|
||||
});
|
||||
|
||||
@ -89,6 +89,10 @@ vi.mock('../infra/config/index.js', () => ({
|
||||
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
||||
updateWorktreeSession: vi.fn(),
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }),
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
global: { provider: 'claude' },
|
||||
project: {},
|
||||
}),
|
||||
saveSessionState: vi.fn(),
|
||||
ensureDir: vi.fn(),
|
||||
writeFileAtomic: vi.fn(),
|
||||
|
||||
@ -16,8 +16,8 @@ function createMovement(overrides: Partial<PieceMovement> = {}): PieceMovement {
|
||||
function createBuilder(step: PieceMovement, engineOverrides: Partial<PieceEngineOptions> = {}): OptionsBuilder {
|
||||
const engineOptions: PieceEngineOptions = {
|
||||
projectCwd: '/project',
|
||||
globalProvider: 'codex',
|
||||
globalProviderProfiles: {
|
||||
provider: 'codex',
|
||||
providerProfiles: {
|
||||
codex: {
|
||||
defaultPermissionMode: 'full',
|
||||
},
|
||||
@ -60,10 +60,8 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
||||
it('uses default profile when provider_profiles are not provided', () => {
|
||||
const step = createMovement();
|
||||
const builder = createBuilder(step, {
|
||||
globalProvider: undefined,
|
||||
globalProviderProfiles: undefined,
|
||||
projectProvider: undefined,
|
||||
provider: undefined,
|
||||
providerProfiles: undefined,
|
||||
});
|
||||
|
||||
const options = builder.buildBaseOptions(step);
|
||||
@ -78,11 +76,8 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
||||
},
|
||||
});
|
||||
const builder = createBuilder(step, {
|
||||
globalProviderOptions: {
|
||||
providerOptions: {
|
||||
codex: { networkAccess: true },
|
||||
claude: { sandbox: { allowUnsandboxedCommands: false } },
|
||||
},
|
||||
projectProviderOptions: {
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
opencode: { networkAccess: true },
|
||||
},
|
||||
@ -105,10 +100,7 @@ describe('OptionsBuilder.buildBaseOptions', () => {
|
||||
it('falls back to global/project provider options when movement has none', () => {
|
||||
const step = createMovement();
|
||||
const builder = createBuilder(step, {
|
||||
globalProviderOptions: {
|
||||
codex: { networkAccess: true },
|
||||
},
|
||||
projectProviderOptions: {
|
||||
providerOptions: {
|
||||
codex: { networkAccess: false },
|
||||
},
|
||||
});
|
||||
|
||||
@ -17,6 +17,17 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||
loadConfig: () => ({
|
||||
global: {
|
||||
language: 'en',
|
||||
enableBuiltinPieces: false,
|
||||
disabledBuiltins: [],
|
||||
},
|
||||
project: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
const { listPieces } = await import('../infra/config/loaders/pieceLoader.js');
|
||||
|
||||
const SAMPLE_PIECE = `name: test-piece
|
||||
|
||||
@ -22,12 +22,21 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
return {
|
||||
...original,
|
||||
getLanguage: () => languageState.value,
|
||||
getBuiltinPiecesEnabled: () => true,
|
||||
getDisabledBuiltins: () => [],
|
||||
loadGlobalConfig: () => ({}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../infra/config/loadConfig.js', () => ({
|
||||
loadConfig: () => ({
|
||||
global: {
|
||||
language: languageState.value,
|
||||
enableBuiltinPieces: true,
|
||||
disabledBuiltins: [],
|
||||
},
|
||||
project: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/resources/index.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
return {
|
||||
@ -92,7 +101,7 @@ describe('piece category config loading', () => {
|
||||
});
|
||||
|
||||
it('should return null when builtin categories file is missing', () => {
|
||||
const config = getPieceCategories();
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
@ -104,7 +113,7 @@ piece_categories:
|
||||
- default
|
||||
`);
|
||||
|
||||
const config = loadDefaultCategories();
|
||||
const config = loadDefaultCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Quick Start', pieces: ['default'], children: [] },
|
||||
@ -125,7 +134,7 @@ show_others_category: true
|
||||
others_category_name: Others
|
||||
`);
|
||||
|
||||
const config = getPieceCategories();
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
@ -165,7 +174,7 @@ show_others_category: false
|
||||
others_category_name: Unclassified
|
||||
`);
|
||||
|
||||
const config = getPieceCategories();
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{
|
||||
@ -207,7 +216,7 @@ piece_categories:
|
||||
- e2e-test
|
||||
`);
|
||||
|
||||
const config = getPieceCategories();
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'レビュー', pieces: ['review-only', 'e2e-test'], children: [] },
|
||||
@ -232,7 +241,7 @@ show_others_category: false
|
||||
others_category_name: Unclassified
|
||||
`);
|
||||
|
||||
const config = getPieceCategories();
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
@ -278,7 +287,7 @@ describe('buildCategorizedPieces', () => {
|
||||
othersCategoryName: 'Others',
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
|
||||
expect(categorized.categories).toEqual([
|
||||
{
|
||||
name: 'Main',
|
||||
@ -310,7 +319,7 @@ describe('buildCategorizedPieces', () => {
|
||||
othersCategoryName: 'Others',
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
|
||||
expect(categorized.categories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
{ name: 'Others', pieces: ['extra'], children: [] },
|
||||
@ -334,7 +343,7 @@ describe('buildCategorizedPieces', () => {
|
||||
othersCategoryName: 'Others',
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
|
||||
expect(categorized.categories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
]);
|
||||
|
||||
@ -91,6 +91,10 @@ vi.mock('../infra/config/index.js', () => ({
|
||||
loadWorktreeSessions: vi.fn().mockReturnValue({}),
|
||||
updateWorktreeSession: vi.fn(),
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({ provider: 'claude' }),
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
global: { provider: 'claude' },
|
||||
project: {},
|
||||
}),
|
||||
saveSessionState: vi.fn(),
|
||||
ensureDir: vi.fn(),
|
||||
writeFileAtomic: vi.fn(),
|
||||
|
||||
@ -31,13 +31,14 @@ describe('resetCategoriesToDefault', () => {
|
||||
|
||||
it('should reset user category overlay and show updated message', async () => {
|
||||
// Given
|
||||
const cwd = '/tmp/test-cwd';
|
||||
|
||||
// When
|
||||
await resetCategoriesToDefault();
|
||||
await resetCategoriesToDefault(cwd);
|
||||
|
||||
// Then
|
||||
expect(mockHeader).toHaveBeenCalledWith('Reset Categories');
|
||||
expect(mockResetPieceCategories).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetPieceCategories).toHaveBeenCalledWith(cwd);
|
||||
expect(mockSuccess).toHaveBeenCalledWith('User category overlay reset.');
|
||||
expect(mockInfo).toHaveBeenCalledWith(' /tmp/user-piece-categories.yaml');
|
||||
});
|
||||
|
||||
@ -5,13 +5,12 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { TaskInfo } from '../infra/task/index.js';
|
||||
|
||||
const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockLoadGlobalConfig, mockLoadProjectConfig, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } =
|
||||
const { mockResolveTaskExecution, mockExecutePiece, mockLoadPieceByIdentifier, mockResolveConfigValues, mockBuildTaskResult, mockPersistTaskResult, mockPersistTaskError, mockPostExecutionFlow } =
|
||||
vi.hoisted(() => ({
|
||||
mockResolveTaskExecution: vi.fn(),
|
||||
mockExecutePiece: vi.fn(),
|
||||
mockLoadPieceByIdentifier: vi.fn(),
|
||||
mockLoadGlobalConfig: vi.fn(),
|
||||
mockLoadProjectConfig: vi.fn(),
|
||||
mockResolveConfigValues: vi.fn(),
|
||||
mockBuildTaskResult: vi.fn(),
|
||||
mockPersistTaskResult: vi.fn(),
|
||||
mockPersistTaskError: vi.fn(),
|
||||
@ -39,10 +38,7 @@ vi.mock('../features/tasks/execute/postExecution.js', () => ({
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||
isPiecePath: () => false,
|
||||
loadConfig: () => ({
|
||||
global: mockLoadGlobalConfig(),
|
||||
project: mockLoadProjectConfig(),
|
||||
}),
|
||||
resolveConfigValues: (...args: unknown[]) => mockResolveConfigValues(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
@ -87,21 +83,19 @@ describe('executeAndCompleteTask', () => {
|
||||
name: 'default',
|
||||
movements: [],
|
||||
});
|
||||
mockLoadGlobalConfig.mockReturnValue({
|
||||
mockResolveConfigValues.mockReturnValue({
|
||||
language: 'en',
|
||||
provider: 'claude',
|
||||
model: undefined,
|
||||
personaProviders: {},
|
||||
providerProfiles: {},
|
||||
providerOptions: {
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
},
|
||||
});
|
||||
mockLoadProjectConfig.mockReturnValue({
|
||||
provider: 'claude',
|
||||
providerProfiles: {},
|
||||
providerOptions: {
|
||||
opencode: { networkAccess: true },
|
||||
},
|
||||
notificationSound: true,
|
||||
notificationSoundEvents: {},
|
||||
concurrency: 1,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
mockBuildTaskResult.mockReturnValue({ success: true });
|
||||
mockResolveTaskExecution.mockResolvedValue({
|
||||
@ -140,16 +134,12 @@ describe('executeAndCompleteTask', () => {
|
||||
const pieceExecutionOptions = mockExecutePiece.mock.calls[0]?.[3] as {
|
||||
taskDisplayLabel?: string;
|
||||
taskPrefix?: string;
|
||||
globalProviderOptions?: unknown;
|
||||
projectProviderOptions?: unknown;
|
||||
providerOptions?: unknown;
|
||||
};
|
||||
expect(pieceExecutionOptions?.taskDisplayLabel).toBe(taskDisplayLabel);
|
||||
expect(pieceExecutionOptions?.taskPrefix).toBe(taskDisplayLabel);
|
||||
expect(pieceExecutionOptions?.globalProviderOptions).toEqual({
|
||||
expect(pieceExecutionOptions?.providerOptions).toEqual({
|
||||
claude: { sandbox: { allowUnsandboxedCommands: true } },
|
||||
});
|
||||
expect(pieceExecutionOptions?.projectProviderOptions).toEqual({
|
||||
opencode: { networkAccess: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -48,7 +48,7 @@ vi.mock('../infra/task/index.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
loadGlobalConfig: vi.fn(() => ({ interactivePreviewMovements: 3, language: 'en' })),
|
||||
resolveConfigValues: vi.fn(() => ({ interactivePreviewMovements: 3, language: 'en' })),
|
||||
getPieceDescription: vi.fn(() => ({
|
||||
name: 'default',
|
||||
description: 'desc',
|
||||
|
||||
@ -4,7 +4,7 @@ const {
|
||||
mockExistsSync,
|
||||
mockSelectPiece,
|
||||
mockSelectOption,
|
||||
mockLoadGlobalConfig,
|
||||
mockResolveConfigValue,
|
||||
mockLoadPieceByIdentifier,
|
||||
mockGetPieceDescription,
|
||||
mockRunRetryMode,
|
||||
@ -16,7 +16,7 @@ const {
|
||||
mockExistsSync: vi.fn(() => true),
|
||||
mockSelectPiece: vi.fn(),
|
||||
mockSelectOption: vi.fn(),
|
||||
mockLoadGlobalConfig: vi.fn(),
|
||||
mockResolveConfigValue: vi.fn(),
|
||||
mockLoadPieceByIdentifier: vi.fn(),
|
||||
mockGetPieceDescription: vi.fn(() => ({
|
||||
name: 'default',
|
||||
@ -60,7 +60,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
loadGlobalConfig: (...args: unknown[]) => mockLoadGlobalConfig(...args),
|
||||
resolveConfigValue: (...args: unknown[]) => mockResolveConfigValue(...args),
|
||||
loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
|
||||
getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
|
||||
}));
|
||||
@ -126,7 +126,7 @@ beforeEach(() => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
mockSelectPiece.mockResolvedValue('default');
|
||||
mockLoadGlobalConfig.mockReturnValue({ defaultPiece: 'default' });
|
||||
mockResolveConfigValue.mockReturnValue(3);
|
||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
||||
mockSelectOption.mockResolvedValue('plan');
|
||||
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { basename, dirname } from 'node:path';
|
||||
import { loadCustomAgents, loadAgentPrompt, loadConfig } from '../infra/config/index.js';
|
||||
import { loadCustomAgents, loadAgentPrompt, resolveConfigValues } from '../infra/config/index.js';
|
||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||
import { createLogger } from '../shared/utils/index.js';
|
||||
@ -29,17 +29,10 @@ export class AgentRunner {
|
||||
agentConfig?: CustomAgentConfig,
|
||||
): ProviderType {
|
||||
if (options?.provider) return options.provider;
|
||||
const config = loadConfig(cwd);
|
||||
const projectConfig = config.project;
|
||||
if (projectConfig.provider) return projectConfig.provider;
|
||||
const config = resolveConfigValues(cwd, ['provider']);
|
||||
if (config.provider) return config.provider;
|
||||
if (options?.stepProvider) return options.stepProvider;
|
||||
if (agentConfig?.provider) return agentConfig.provider;
|
||||
try {
|
||||
const globalConfig = config.global;
|
||||
if (globalConfig.provider) return globalConfig.provider;
|
||||
} catch (error) {
|
||||
log.debug('Global config not available for provider resolution', { error });
|
||||
}
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
@ -57,14 +50,10 @@ export class AgentRunner {
|
||||
if (options?.stepModel) return options.stepModel;
|
||||
if (agentConfig?.model) return agentConfig.model;
|
||||
if (!options?.cwd) return undefined;
|
||||
try {
|
||||
const globalConfig = loadConfig(options.cwd).global;
|
||||
if (globalConfig.model) {
|
||||
const globalProvider = globalConfig.provider ?? 'claude';
|
||||
if (globalProvider === resolvedProvider) return globalConfig.model;
|
||||
}
|
||||
} catch (error) {
|
||||
log.debug('Global config not available for model resolution', { error });
|
||||
const config = resolveConfigValues(options.cwd, ['provider', 'model']);
|
||||
if (config.model) {
|
||||
const defaultProvider = config.provider ?? 'claude';
|
||||
if (defaultProvider === resolvedProvider) return config.model;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -133,7 +122,7 @@ export class AgentRunner {
|
||||
name: agentConfig.name,
|
||||
systemPrompt: agentConfig.claudeAgent || agentConfig.claudeSkill
|
||||
? undefined
|
||||
: loadAgentPrompt(agentConfig),
|
||||
: loadAgentPrompt(agentConfig, options.cwd),
|
||||
claudeAgent: agentConfig.claudeAgent,
|
||||
claudeSkill: agentConfig.claudeSkill,
|
||||
});
|
||||
|
||||
@ -104,7 +104,7 @@ reset
|
||||
.command('categories')
|
||||
.description('Reset piece categories to builtin defaults')
|
||||
.action(async () => {
|
||||
await resetCategoriesToDefault();
|
||||
await resetCategoriesToDefault(resolvedCwd);
|
||||
});
|
||||
|
||||
program
|
||||
|
||||
@ -11,7 +11,7 @@ import { resolve } from 'node:path';
|
||||
import {
|
||||
initGlobalDirs,
|
||||
initProjectDirs,
|
||||
loadConfig,
|
||||
resolveConfigValues,
|
||||
isVerboseMode,
|
||||
} from '../../infra/config/index.js';
|
||||
import { setQuietMode } from '../../shared/context.js';
|
||||
@ -69,7 +69,7 @@ export async function runPreActionHook(): Promise<void> {
|
||||
const verbose = isVerboseMode(resolvedCwd);
|
||||
initDebugLogger(verbose ? { enabled: true } : undefined, resolvedCwd);
|
||||
|
||||
const { global: config } = loadConfig(resolvedCwd);
|
||||
const config = resolveConfigValues(resolvedCwd, ['logLevel', 'minimalOutput']);
|
||||
|
||||
if (verbose) {
|
||||
setVerboseConsole(true);
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
dispatchConversationAction,
|
||||
type InteractiveModeResult,
|
||||
} from '../../features/interactive/index.js';
|
||||
import { getPieceDescription, loadConfig } from '../../infra/config/index.js';
|
||||
import { getPieceDescription, resolveConfigValues } from '../../infra/config/index.js';
|
||||
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
||||
import { program, resolvedCwd, pipelineMode } from './program.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
|
||||
const { global: globalConfig } = loadConfig(resolvedCwd);
|
||||
const globalConfig = resolveConfigValues(resolvedCwd, ['language', 'interactivePreviewMovements', 'provider']);
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
|
||||
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
||||
|
||||
@ -56,9 +56,7 @@ export class OptionsBuilder {
|
||||
|
||||
const resolvedProviderForPermissions =
|
||||
this.engineOptions.provider
|
||||
?? this.engineOptions.projectProvider
|
||||
?? resolved.provider
|
||||
?? this.engineOptions.globalProvider
|
||||
?? 'claude';
|
||||
|
||||
return {
|
||||
@ -73,12 +71,11 @@ export class OptionsBuilder {
|
||||
movementName: step.name,
|
||||
requiredPermissionMode: step.requiredPermissionMode,
|
||||
provider: resolvedProviderForPermissions,
|
||||
projectProviderProfiles: this.engineOptions.projectProviderProfiles,
|
||||
globalProviderProfiles: this.engineOptions.globalProviderProfiles ?? DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
||||
projectProviderProfiles: this.engineOptions.providerProfiles,
|
||||
globalProviderProfiles: DEFAULT_PROVIDER_PERMISSION_PROFILES,
|
||||
}),
|
||||
providerOptions: mergeProviderOptions(
|
||||
this.engineOptions.globalProviderOptions,
|
||||
this.engineOptions.projectProviderOptions,
|
||||
this.engineOptions.providerOptions,
|
||||
step.providerOptions,
|
||||
),
|
||||
language: this.getLanguage(),
|
||||
|
||||
@ -179,21 +179,13 @@ export interface PieceEngineOptions {
|
||||
/** Language for instruction metadata. Defaults to 'en'. */
|
||||
language?: Language;
|
||||
provider?: ProviderType;
|
||||
/** Project config provider (used for provider/profile resolution parity with AgentRunner) */
|
||||
projectProvider?: ProviderType;
|
||||
/** Global config provider (used for provider/profile resolution parity with AgentRunner) */
|
||||
globalProvider?: ProviderType;
|
||||
model?: string;
|
||||
/** Project-level provider options */
|
||||
projectProviderOptions?: MovementProviderOptions;
|
||||
/** Global-level provider options */
|
||||
globalProviderOptions?: MovementProviderOptions;
|
||||
/** Resolved provider options */
|
||||
providerOptions?: MovementProviderOptions;
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
personaProviders?: Record<string, ProviderType>;
|
||||
/** Project-level provider permission profiles */
|
||||
projectProviderProfiles?: ProviderPermissionProfiles;
|
||||
/** Global-level provider permission profiles */
|
||||
globalProviderProfiles?: ProviderPermissionProfiles;
|
||||
/** Resolved provider permission profiles */
|
||||
providerProfiles?: ProviderPermissionProfiles;
|
||||
/** Enable interactive-only rules and user-input transitions */
|
||||
interactive?: boolean;
|
||||
/** Rule tag index detector (required for rules evaluation) */
|
||||
|
||||
@ -11,7 +11,7 @@ import chalk from 'chalk';
|
||||
import type { PieceSource } from '../../infra/config/loaders/pieceResolver.js';
|
||||
import { getLanguageResourcesDir } from '../../infra/resources/index.js';
|
||||
import { getGlobalConfigDir, getProjectConfigDir } from '../../infra/config/paths.js';
|
||||
import { getLanguage, getBuiltinPiecesEnabled } from '../../infra/config/global/globalConfig.js';
|
||||
import { resolveConfigValues } from '../../infra/config/index.js';
|
||||
import { section, error as logError, info } from '../../shared/ui/index.js';
|
||||
|
||||
const FACET_TYPES = [
|
||||
@ -62,10 +62,11 @@ function getFacetDirs(
|
||||
facetType: FacetType,
|
||||
cwd: string,
|
||||
): { dir: string; source: PieceSource }[] {
|
||||
const config = resolveConfigValues(cwd, ['enableBuiltinPieces', 'language']);
|
||||
const dirs: { dir: string; source: PieceSource }[] = [];
|
||||
|
||||
if (getBuiltinPiecesEnabled()) {
|
||||
const lang = getLanguage();
|
||||
if (config.enableBuiltinPieces !== false) {
|
||||
const lang = config.language;
|
||||
dirs.push({ dir: join(getLanguageResourcesDir(lang), facetType), source: 'builtin' });
|
||||
}
|
||||
|
||||
|
||||
@ -5,12 +5,12 @@
|
||||
import { resetPieceCategories, getPieceCategoriesPath } from '../../infra/config/global/pieceCategories.js';
|
||||
import { header, success, info } from '../../shared/ui/index.js';
|
||||
|
||||
export async function resetCategoriesToDefault(): Promise<void> {
|
||||
export async function resetCategoriesToDefault(cwd: string): Promise<void> {
|
||||
header('Reset Categories');
|
||||
|
||||
resetPieceCategories();
|
||||
resetPieceCategories(cwd);
|
||||
|
||||
const userPath = getPieceCategoriesPath();
|
||||
const userPath = getPieceCategoriesPath(cwd);
|
||||
success('User category overlay reset.');
|
||||
info(` ${userPath}`);
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
loadConfig,
|
||||
resolveConfigValues,
|
||||
loadPersonaSessions,
|
||||
updatePersonaSession,
|
||||
loadSessionState,
|
||||
@ -58,7 +58,7 @@ export interface SessionContext {
|
||||
* Initialize provider, session, and language for interactive conversation.
|
||||
*/
|
||||
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
const globalConfig = resolveConfigValues(cwd, ['language', 'provider', 'model']);
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
if (!globalConfig.provider) {
|
||||
throw new Error('Provider is not configured.');
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
import { resolveLanguage } from './interactive.js';
|
||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||
import { getLabelObject } from '../../shared/i18n/index.js';
|
||||
import { loadConfig } from '../../infra/config/index.js';
|
||||
import { resolveConfigValues } from '../../infra/config/index.js';
|
||||
import type { InstructModeResult, InstructUIText } from '../tasks/list/instructMode.js';
|
||||
|
||||
/** Failure information for a retry task */
|
||||
@ -116,7 +116,7 @@ export async function runRetryMode(
|
||||
cwd: string,
|
||||
retryContext: RetryContext,
|
||||
): Promise<InstructModeResult> {
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
const globalConfig = resolveConfigValues(cwd, ['language', 'provider']);
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
|
||||
if (!globalConfig.provider) {
|
||||
|
||||
@ -521,7 +521,7 @@ export async function selectPiece(
|
||||
options?: SelectPieceOptions,
|
||||
): Promise<string | null> {
|
||||
const fallbackToDefault = options?.fallbackToDefault !== false;
|
||||
const categoryConfig = getPieceCategories();
|
||||
const categoryConfig = getPieceCategories(cwd);
|
||||
const currentPiece = getCurrentPiece(cwd);
|
||||
|
||||
if (categoryConfig) {
|
||||
@ -534,7 +534,7 @@ export async function selectPiece(
|
||||
info('No pieces found.');
|
||||
return null;
|
||||
}
|
||||
const categorized = buildCategorizedPieces(allPieces, categoryConfig);
|
||||
const categorized = buildCategorizedPieces(allPieces, categoryConfig, cwd);
|
||||
warnMissingPieces(categorized.missingPieces.filter((missing) => missing.source === 'user'));
|
||||
return selectPieceFromCategorizedPieces(categorized, currentPiece);
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
} from '../../infra/github/index.js';
|
||||
import { stageAndCommit, getCurrentBranch } from '../../infra/task/index.js';
|
||||
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
||||
import { loadConfig } from '../../infra/config/index.js';
|
||||
import { resolveConfigValues } from '../../infra/config/index.js';
|
||||
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import type { PipelineConfig } from '../../core/models/index.js';
|
||||
@ -106,7 +106,7 @@ function buildPipelinePrBody(
|
||||
*/
|
||||
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
||||
const { cwd, piece, autoPr, skipGit } = options;
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
const globalConfig = resolveConfigValues(cwd, ['pipeline']);
|
||||
const pipelineConfig = globalConfig.pipeline;
|
||||
let issue: GitHubIssue | undefined;
|
||||
let task: string;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* Useful for debugging and understanding what prompts agents will receive.
|
||||
*/
|
||||
|
||||
import { loadPieceByIdentifier, getCurrentPiece, loadConfig } from '../../infra/config/index.js';
|
||||
import { loadPieceByIdentifier, getCurrentPiece, resolveConfigValue } from '../../infra/config/index.js';
|
||||
import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js';
|
||||
import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js';
|
||||
import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js';
|
||||
@ -29,8 +29,7 @@ export async function previewPrompts(cwd: string, pieceIdentifier?: string): Pro
|
||||
return;
|
||||
}
|
||||
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
const language: Language = globalConfig.language ?? 'en';
|
||||
const language = resolveConfigValue(cwd, 'language') as Language;
|
||||
|
||||
header(`Prompt Preview: ${config.name}`);
|
||||
info(`Movements: ${config.movements.length}`);
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
updatePersonaSession,
|
||||
loadWorktreeSessions,
|
||||
updateWorktreeSession,
|
||||
loadConfig,
|
||||
resolveConfigValues,
|
||||
saveSessionState,
|
||||
type SessionState,
|
||||
} from '../../../infra/config/index.js';
|
||||
@ -317,7 +317,10 @@ export async function executePiece(
|
||||
|
||||
// Load saved agent sessions only on retry; normal runs start with empty sessions
|
||||
const isWorktree = cwd !== projectCwd;
|
||||
const { global: globalConfig } = loadConfig(projectCwd);
|
||||
const globalConfig = resolveConfigValues(
|
||||
projectCwd,
|
||||
['notificationSound', 'notificationSoundEvents', 'provider', 'runtime', 'preventSleep', 'model', 'observability'],
|
||||
);
|
||||
const shouldNotify = globalConfig.notificationSound !== false;
|
||||
const notificationSoundEvents = globalConfig.notificationSoundEvents;
|
||||
const shouldNotifyIterationLimit = shouldNotify && notificationSoundEvents?.iterationLimit !== false;
|
||||
@ -443,14 +446,10 @@ export async function executePiece(
|
||||
projectCwd,
|
||||
language: options.language,
|
||||
provider: options.provider,
|
||||
projectProvider: options.projectProvider,
|
||||
globalProvider: options.globalProvider,
|
||||
model: options.model,
|
||||
projectProviderOptions: options.projectProviderOptions,
|
||||
globalProviderOptions: options.globalProviderOptions,
|
||||
providerOptions: options.providerOptions,
|
||||
personaProviders: options.personaProviders,
|
||||
projectProviderProfiles: options.projectProviderProfiles,
|
||||
globalProviderProfiles: options.globalProviderProfiles,
|
||||
providerProfiles: options.providerProfiles,
|
||||
interactive: interactiveUserInput,
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* instructBranch (instruct mode from takt list).
|
||||
*/
|
||||
|
||||
import { loadConfig } from '../../../infra/config/index.js';
|
||||
import { resolveConfigValue } from '../../../infra/config/index.js';
|
||||
import { confirm } from '../../../shared/prompt/index.js';
|
||||
import { autoCommitAndPush } from '../../../infra/task/index.js';
|
||||
import { info, error, success } from '../../../shared/ui/index.js';
|
||||
@ -23,9 +23,9 @@ export async function resolveAutoPr(optionAutoPr: boolean | undefined, cwd: stri
|
||||
return optionAutoPr;
|
||||
}
|
||||
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
if (typeof globalConfig.autoPr === 'boolean') {
|
||||
return globalConfig.autoPr;
|
||||
const autoPr = resolveConfigValue(cwd, 'autoPr');
|
||||
if (typeof autoPr === 'boolean') {
|
||||
return autoPr;
|
||||
}
|
||||
|
||||
return confirm('Create pull request?', true);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { loadConfig } from '../../../infra/config/index.js';
|
||||
import { resolveConfigValue } from '../../../infra/config/index.js';
|
||||
import { type TaskInfo, createSharedClone, summarizeTaskName, getCurrentBranch } from '../../../infra/task/index.js';
|
||||
import { withProgress } from '../../../shared/ui/index.js';
|
||||
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
||||
@ -141,8 +141,7 @@ export async function resolveTaskExecution(
|
||||
if (data.auto_pr !== undefined) {
|
||||
autoPr = data.auto_pr;
|
||||
} else {
|
||||
const { global: globalConfig } = loadConfig(defaultCwd);
|
||||
autoPr = globalConfig.autoPr ?? false;
|
||||
autoPr = resolveConfigValue(defaultCwd, 'autoPr') ?? false;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Session management helpers for agent execution
|
||||
*/
|
||||
|
||||
import { loadPersonaSessions, updatePersonaSession, loadConfig } from '../../../infra/config/index.js';
|
||||
import { loadPersonaSessions, updatePersonaSession, resolveConfigValue } from '../../../infra/config/index.js';
|
||||
import type { AgentResponse } from '../../../core/models/index.js';
|
||||
|
||||
/**
|
||||
@ -15,7 +15,7 @@ export async function withPersonaSession(
|
||||
fn: (sessionId?: string) => Promise<AgentResponse>,
|
||||
provider?: string
|
||||
): Promise<AgentResponse> {
|
||||
const resolvedProvider = provider ?? loadConfig(cwd).global.provider ?? 'claude';
|
||||
const resolvedProvider = provider ?? resolveConfigValue(cwd, 'provider') ?? 'claude';
|
||||
const sessions = loadPersonaSessions(cwd, resolvedProvider);
|
||||
const sessionId = sessions[personaName];
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Task execution logic
|
||||
*/
|
||||
|
||||
import { loadPieceByIdentifier, isPiecePath, loadConfig } from '../../../infra/config/index.js';
|
||||
import { loadPieceByIdentifier, isPiecePath, resolveConfigValues } from '../../../infra/config/index.js';
|
||||
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
||||
import {
|
||||
header,
|
||||
@ -86,21 +86,22 @@ async function executeTaskWithResult(options: ExecuteTaskOptions): Promise<Piece
|
||||
movements: pieceConfig.movements.map((s: { name: string }) => s.name),
|
||||
});
|
||||
|
||||
const config = loadConfig(projectCwd);
|
||||
const globalConfig = config.global;
|
||||
const projectConfig = config.project;
|
||||
const config = resolveConfigValues(projectCwd, [
|
||||
'language',
|
||||
'provider',
|
||||
'model',
|
||||
'providerOptions',
|
||||
'personaProviders',
|
||||
'providerProfiles',
|
||||
]);
|
||||
return await executePiece(pieceConfig, task, cwd, {
|
||||
projectCwd,
|
||||
language: globalConfig.language,
|
||||
provider: agentOverrides?.provider,
|
||||
projectProvider: projectConfig.provider,
|
||||
globalProvider: globalConfig.provider,
|
||||
model: agentOverrides?.model,
|
||||
projectProviderOptions: projectConfig.providerOptions,
|
||||
globalProviderOptions: globalConfig.providerOptions,
|
||||
personaProviders: globalConfig.personaProviders,
|
||||
projectProviderProfiles: projectConfig.providerProfiles,
|
||||
globalProviderProfiles: globalConfig.providerProfiles,
|
||||
language: config.language,
|
||||
provider: agentOverrides?.provider ?? config.provider,
|
||||
model: agentOverrides?.model ?? config.model,
|
||||
providerOptions: config.providerOptions,
|
||||
personaProviders: config.personaProviders,
|
||||
providerProfiles: config.providerProfiles,
|
||||
interactiveUserInput,
|
||||
interactiveMetadata,
|
||||
startMovement,
|
||||
@ -237,7 +238,10 @@ export async function runAllTasks(
|
||||
options?: TaskExecutionOptions,
|
||||
): Promise<void> {
|
||||
const taskRunner = new TaskRunner(cwd);
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
const globalConfig = resolveConfigValues(
|
||||
cwd,
|
||||
['notificationSound', 'notificationSoundEvents', 'concurrency', 'taskPollIntervalMs'],
|
||||
);
|
||||
const shouldNotifyRunComplete = globalConfig.notificationSound !== false
|
||||
&& globalConfig.notificationSoundEvents?.runComplete !== false;
|
||||
const shouldNotifyRunAbort = globalConfig.notificationSound !== false
|
||||
|
||||
@ -33,21 +33,13 @@ export interface PieceExecutionOptions {
|
||||
/** Language for instruction metadata */
|
||||
language?: Language;
|
||||
provider?: ProviderType;
|
||||
/** Project config provider */
|
||||
projectProvider?: ProviderType;
|
||||
/** Global config provider */
|
||||
globalProvider?: ProviderType;
|
||||
model?: string;
|
||||
/** Project-level provider options */
|
||||
projectProviderOptions?: MovementProviderOptions;
|
||||
/** Global-level provider options */
|
||||
globalProviderOptions?: MovementProviderOptions;
|
||||
/** Resolved provider options */
|
||||
providerOptions?: MovementProviderOptions;
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
personaProviders?: Record<string, ProviderType>;
|
||||
/** Project-level provider permission profiles */
|
||||
projectProviderProfiles?: ProviderPermissionProfiles;
|
||||
/** Global-level provider permission profiles */
|
||||
globalProviderProfiles?: ProviderPermissionProfiles;
|
||||
/** Resolved provider permission profiles */
|
||||
providerProfiles?: ProviderPermissionProfiles;
|
||||
/** Enable interactive user input during step transitions */
|
||||
interactiveUserInput?: boolean;
|
||||
/** Interactive mode result metadata for NDJSON logging */
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
import { type RunSessionContext, formatRunSessionForPrompt } from '../../interactive/runSessionReader.js';
|
||||
import { loadTemplate } from '../../../shared/prompts/index.js';
|
||||
import { getLabelObject } from '../../../shared/i18n/index.js';
|
||||
import { loadConfig } from '../../../infra/config/index.js';
|
||||
import { resolveConfigValues } from '../../../infra/config/index.js';
|
||||
|
||||
export type InstructModeAction = 'execute' | 'save_task' | 'cancel';
|
||||
|
||||
@ -109,7 +109,7 @@ export async function runInstructMode(
|
||||
pieceContext?: PieceContext,
|
||||
runSessionContext?: RunSessionContext,
|
||||
): Promise<InstructModeResult> {
|
||||
const { global: globalConfig } = loadConfig(cwd);
|
||||
const globalConfig = resolveConfigValues(cwd, ['language', 'provider']);
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
|
||||
if (!globalConfig.provider) {
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
TaskRunner,
|
||||
detectDefaultBranch,
|
||||
} from '../../../infra/task/index.js';
|
||||
import { loadConfig, getPieceDescription } from '../../../infra/config/index.js';
|
||||
import { resolveConfigValues, getPieceDescription } from '../../../infra/config/index.js';
|
||||
import { info, error as logError } from '../../../shared/ui/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
import { runInstructMode } from './instructMode.js';
|
||||
@ -93,7 +93,7 @@ export async function instructBranch(
|
||||
return false;
|
||||
}
|
||||
|
||||
const { global: globalConfig } = loadConfig(projectDir);
|
||||
const globalConfig = resolveConfigValues(projectDir, ['interactivePreviewMovements', 'language']);
|
||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
||||
const pieceContext: PieceContext = {
|
||||
name: pieceDesc.name,
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import type { TaskListItem } from '../../../infra/task/index.js';
|
||||
import { TaskRunner } from '../../../infra/task/index.js';
|
||||
import { loadPieceByIdentifier, loadConfig, getPieceDescription } from '../../../infra/config/index.js';
|
||||
import { loadPieceByIdentifier, resolveConfigValue, getPieceDescription } from '../../../infra/config/index.js';
|
||||
import { selectPiece } from '../../pieceSelection/index.js';
|
||||
import { selectOption } from '../../../shared/prompt/index.js';
|
||||
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
||||
@ -133,7 +133,7 @@ export async function retryFailedTask(
|
||||
return false;
|
||||
}
|
||||
|
||||
const { global: globalConfig } = loadConfig(projectDir);
|
||||
const previewCount = resolveConfigValue(projectDir, 'interactivePreviewMovements');
|
||||
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
|
||||
|
||||
if (!pieceConfig) {
|
||||
@ -145,7 +145,7 @@ export async function retryFailedTask(
|
||||
return false;
|
||||
}
|
||||
|
||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, globalConfig.interactivePreviewMovements);
|
||||
const pieceDesc = getPieceDescription(selectedPiece, projectDir, previewCount);
|
||||
const pieceContext = {
|
||||
name: pieceDesc.name,
|
||||
description: pieceDesc.description,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { getGlobalConfigDir } from '../paths.js';
|
||||
import { loadGlobalConfig } from './globalConfig.js';
|
||||
import { loadConfig } from '../loadConfig.js';
|
||||
|
||||
const INITIAL_USER_CATEGORIES_CONTENT = 'piece_categories: {}\n';
|
||||
|
||||
@ -16,8 +16,8 @@ function getDefaultPieceCategoriesPath(): string {
|
||||
}
|
||||
|
||||
/** Get the path to the user's piece categories file. */
|
||||
export function getPieceCategoriesPath(): string {
|
||||
const config = loadGlobalConfig();
|
||||
export function getPieceCategoriesPath(cwd: string): string {
|
||||
const config = loadConfig(cwd);
|
||||
if (config.pieceCategoriesFile) {
|
||||
return config.pieceCategoriesFile;
|
||||
}
|
||||
@ -27,8 +27,8 @@ export function getPieceCategoriesPath(): string {
|
||||
/**
|
||||
* Reset user categories overlay file to initial content.
|
||||
*/
|
||||
export function resetPieceCategories(): void {
|
||||
const userPath = getPieceCategoriesPath();
|
||||
export function resetPieceCategories(cwd: string): void {
|
||||
const userPath = getPieceCategoriesPath(cwd);
|
||||
const dir = dirname(userPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
@ -6,4 +6,4 @@ export * from './paths.js';
|
||||
export * from './loaders/index.js';
|
||||
export * from './global/index.js';
|
||||
export * from './project/index.js';
|
||||
export * from './loadConfig.js';
|
||||
export * from './resolveConfigValue.js';
|
||||
|
||||
@ -1,16 +1,108 @@
|
||||
import type { GlobalConfig } from '../../core/models/index.js';
|
||||
import type { ProjectLocalConfig } from './project/projectConfig.js';
|
||||
import type { MovementProviderOptions } from '../../core/models/piece-types.js';
|
||||
import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js';
|
||||
import { loadGlobalConfig } from './global/globalConfig.js';
|
||||
import { loadProjectConfig } from './project/projectConfig.js';
|
||||
import { envVarNameFromPath } from './env/config-env-overrides.js';
|
||||
|
||||
export interface LoadedConfig {
|
||||
global: GlobalConfig;
|
||||
project: ProjectLocalConfig;
|
||||
export interface LoadedConfig extends GlobalConfig {
|
||||
piece: string;
|
||||
verbose: boolean;
|
||||
providerOptions?: MovementProviderOptions;
|
||||
providerProfiles?: ProviderPermissionProfiles;
|
||||
}
|
||||
|
||||
export function loadConfig(projectDir: string): LoadedConfig {
|
||||
const global = loadGlobalConfig();
|
||||
const project = loadProjectConfig(projectDir);
|
||||
const provider = project.provider ?? global.provider;
|
||||
|
||||
return {
|
||||
global: loadGlobalConfig(),
|
||||
project: loadProjectConfig(projectDir),
|
||||
...global,
|
||||
piece: project.piece ?? 'default',
|
||||
provider,
|
||||
model: resolveModel(global, provider),
|
||||
verbose: resolveVerbose(project.verbose, global.verbose),
|
||||
providerOptions: mergeProviderOptions(global.providerOptions, project.providerOptions),
|
||||
providerProfiles: mergeProviderProfiles(global.providerProfiles, project.providerProfiles),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveModel(global: GlobalConfig, provider: GlobalConfig['provider']): string | undefined {
|
||||
if (!global.model) return undefined;
|
||||
const globalProvider = global.provider ?? 'claude';
|
||||
const resolvedProvider = provider ?? 'claude';
|
||||
if (globalProvider !== resolvedProvider) return undefined;
|
||||
return global.model;
|
||||
}
|
||||
|
||||
function resolveVerbose(projectVerbose: boolean | undefined, globalVerbose: boolean | undefined): boolean {
|
||||
const envVerbose = loadEnvBooleanSetting('verbose');
|
||||
if (envVerbose !== undefined) return envVerbose;
|
||||
if (projectVerbose !== undefined) return projectVerbose;
|
||||
if (globalVerbose !== undefined) return globalVerbose;
|
||||
return false;
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
function mergeProviderOptions(
|
||||
globalOptions: MovementProviderOptions | undefined,
|
||||
projectOptions: MovementProviderOptions | undefined,
|
||||
): MovementProviderOptions | undefined {
|
||||
if (!globalOptions && !projectOptions) return undefined;
|
||||
|
||||
const result: MovementProviderOptions = {};
|
||||
if (globalOptions?.codex || projectOptions?.codex) {
|
||||
result.codex = { ...globalOptions?.codex, ...projectOptions?.codex };
|
||||
}
|
||||
if (globalOptions?.opencode || projectOptions?.opencode) {
|
||||
result.opencode = { ...globalOptions?.opencode, ...projectOptions?.opencode };
|
||||
}
|
||||
if (globalOptions?.claude?.sandbox || projectOptions?.claude?.sandbox) {
|
||||
result.claude = {
|
||||
sandbox: {
|
||||
...globalOptions?.claude?.sandbox,
|
||||
...projectOptions?.claude?.sandbox,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
function mergeProviderProfiles(
|
||||
globalProfiles: ProviderPermissionProfiles | undefined,
|
||||
projectProfiles: ProviderPermissionProfiles | undefined,
|
||||
): ProviderPermissionProfiles | undefined {
|
||||
if (!globalProfiles && !projectProfiles) return undefined;
|
||||
|
||||
const merged: ProviderPermissionProfiles = { ...(globalProfiles ?? {}) };
|
||||
for (const [provider, profile] of Object.entries(projectProfiles ?? {})) {
|
||||
const key = provider as keyof ProviderPermissionProfiles;
|
||||
const existing = merged[key];
|
||||
if (!existing) {
|
||||
merged[key] = profile;
|
||||
continue;
|
||||
}
|
||||
merged[key] = {
|
||||
defaultPermissionMode: profile.defaultPermissionMode,
|
||||
movementPermissionOverrides: {
|
||||
...(existing.movementPermissionOverrides ?? {}),
|
||||
...(profile.movementPermissionOverrides ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
|
||||
@ -16,11 +16,11 @@ import {
|
||||
getBuiltinPiecesDir,
|
||||
isPathSafe,
|
||||
} from '../paths.js';
|
||||
import { getLanguage } from '../global/globalConfig.js';
|
||||
import { loadConfig } from '../loadConfig.js';
|
||||
|
||||
/** Get all allowed base directories for persona prompt files */
|
||||
function getAllowedPromptBases(): string[] {
|
||||
const lang = getLanguage();
|
||||
function getAllowedPromptBases(cwd: string): string[] {
|
||||
const lang = loadConfig(cwd).language;
|
||||
return [
|
||||
getGlobalPersonasDir(),
|
||||
getGlobalPiecesDir(),
|
||||
@ -63,14 +63,14 @@ export function listCustomAgents(): string[] {
|
||||
}
|
||||
|
||||
/** Load agent prompt content. */
|
||||
export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
||||
export function loadAgentPrompt(agent: CustomAgentConfig, cwd: string): string {
|
||||
if (agent.prompt) {
|
||||
return agent.prompt;
|
||||
}
|
||||
|
||||
if (agent.promptFile) {
|
||||
const promptFile = agent.promptFile;
|
||||
const isValid = getAllowedPromptBases().some((base) => isPathSafe(base, promptFile));
|
||||
const isValid = getAllowedPromptBases(cwd).some((base) => isPathSafe(base, promptFile));
|
||||
if (!isValid) {
|
||||
throw new Error(`Agent prompt file path is not allowed: ${agent.promptFile}`);
|
||||
}
|
||||
@ -86,8 +86,8 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
||||
}
|
||||
|
||||
/** Load persona prompt from a resolved path. */
|
||||
export function loadPersonaPromptFromPath(personaPath: string): string {
|
||||
const isValid = getAllowedPromptBases().some((base) => isPathSafe(base, personaPath));
|
||||
export function loadPersonaPromptFromPath(personaPath: string, cwd: string): string {
|
||||
const isValid = getAllowedPromptBases(cwd).some((base) => isPathSafe(base, personaPath));
|
||||
if (!isValid) {
|
||||
throw new Error(`Persona prompt file path is not allowed: ${personaPath}`);
|
||||
}
|
||||
|
||||
@ -10,10 +10,10 @@ import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { z } from 'zod/v4';
|
||||
import { getLanguage, getBuiltinPiecesEnabled, getDisabledBuiltins } from '../global/globalConfig.js';
|
||||
import { getPieceCategoriesPath } from '../global/pieceCategories.js';
|
||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
||||
import { listBuiltinPieceNames } from './pieceResolver.js';
|
||||
import { loadConfig } from '../loadConfig.js';
|
||||
import type { PieceWithSource } from './pieceResolver.js';
|
||||
|
||||
const CategoryConfigSchema = z.object({
|
||||
@ -232,8 +232,8 @@ function resolveOthersCategoryName(defaultConfig: ParsedCategoryConfig, userConf
|
||||
* Load default categories from builtin resource file.
|
||||
* Returns null if file doesn't exist or has no piece_categories.
|
||||
*/
|
||||
export function loadDefaultCategories(): CategoryConfig | null {
|
||||
const lang = getLanguage();
|
||||
export function loadDefaultCategories(cwd: string): CategoryConfig | null {
|
||||
const lang = loadConfig(cwd).language;
|
||||
const filePath = join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
||||
const parsed = loadCategoryConfigFromPath(filePath, filePath);
|
||||
|
||||
@ -255,8 +255,8 @@ export function loadDefaultCategories(): CategoryConfig | null {
|
||||
}
|
||||
|
||||
/** Get the path to the builtin default categories file. */
|
||||
export function getDefaultCategoriesPath(): string {
|
||||
const lang = getLanguage();
|
||||
export function getDefaultCategoriesPath(cwd: string): string {
|
||||
const lang = loadConfig(cwd).language;
|
||||
return join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
||||
}
|
||||
|
||||
@ -264,14 +264,14 @@ export function getDefaultCategoriesPath(): string {
|
||||
* Get effective piece categories configuration.
|
||||
* Built from builtin categories and optional user overlay.
|
||||
*/
|
||||
export function getPieceCategories(): CategoryConfig | null {
|
||||
const defaultPath = getDefaultCategoriesPath();
|
||||
export function getPieceCategories(cwd: string): CategoryConfig | null {
|
||||
const defaultPath = getDefaultCategoriesPath(cwd);
|
||||
const defaultConfig = loadCategoryConfigFromPath(defaultPath, defaultPath);
|
||||
if (!defaultConfig?.pieceCategories) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userPath = getPieceCategoriesPath();
|
||||
const userPath = getPieceCategoriesPath(cwd);
|
||||
const userConfig = loadCategoryConfigFromPath(userPath, userPath);
|
||||
|
||||
const merged = userConfig?.pieceCategories
|
||||
@ -376,14 +376,16 @@ function appendOthersCategory(
|
||||
export function buildCategorizedPieces(
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
config: CategoryConfig,
|
||||
cwd: string,
|
||||
): CategorizedPieces {
|
||||
const globalConfig = loadConfig(cwd);
|
||||
const ignoreMissing = new Set<string>();
|
||||
if (!getBuiltinPiecesEnabled()) {
|
||||
for (const name of listBuiltinPieceNames({ includeDisabled: true })) {
|
||||
if (globalConfig.enableBuiltinPieces === false) {
|
||||
for (const name of listBuiltinPieceNames(cwd, { includeDisabled: true })) {
|
||||
ignoreMissing.add(name);
|
||||
}
|
||||
} else {
|
||||
for (const name of getDisabledBuiltins()) {
|
||||
for (const name of (globalConfig.disabledBuiltins ?? [])) {
|
||||
ignoreMissing.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import { parse as parseYaml } from 'yaml';
|
||||
import type { z } from 'zod';
|
||||
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
|
||||
import type { PieceConfig, PieceMovement, PieceRule, OutputContractEntry, OutputContractItem, LoopMonitorConfig, LoopMonitorJudge, ArpeggioMovementConfig, ArpeggioMergeMovementConfig, TeamLeaderConfig } from '../../../core/models/index.js';
|
||||
import { getLanguage } from '../global/globalConfig.js';
|
||||
import { loadConfig } from '../loadConfig.js';
|
||||
import {
|
||||
type PieceSections,
|
||||
type FacetResolutionContext,
|
||||
@ -428,9 +428,9 @@ export function normalizePieceConfig(
|
||||
/**
|
||||
* Load a piece from a YAML file.
|
||||
* @param filePath Path to the piece YAML file
|
||||
* @param projectDir Optional project directory for 3-layer facet resolution
|
||||
* @param projectDir Project directory for 3-layer facet resolution
|
||||
*/
|
||||
export function loadPieceFromFile(filePath: string, projectDir?: string): PieceConfig {
|
||||
export function loadPieceFromFile(filePath: string, projectDir: string): PieceConfig {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error(`Piece file not found: ${filePath}`);
|
||||
}
|
||||
@ -439,7 +439,7 @@ export function loadPieceFromFile(filePath: string, projectDir?: string): PieceC
|
||||
const pieceDir = dirname(filePath);
|
||||
|
||||
const context: FacetResolutionContext = {
|
||||
lang: getLanguage(),
|
||||
lang: loadConfig(projectDir).language,
|
||||
projectDir,
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import { join, resolve, isAbsolute } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { PieceConfig, PieceMovement, InteractiveMode } from '../../../core/models/index.js';
|
||||
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js';
|
||||
import { getLanguage, getDisabledBuiltins, getBuiltinPiecesEnabled } from '../global/globalConfig.js';
|
||||
import { loadConfig } from '../loadConfig.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
import { loadPieceFromFile } from './pieceParser.js';
|
||||
|
||||
@ -23,10 +23,11 @@ export interface PieceWithSource {
|
||||
source: PieceSource;
|
||||
}
|
||||
|
||||
export function listBuiltinPieceNames(options?: { includeDisabled?: boolean }): string[] {
|
||||
const lang = getLanguage();
|
||||
export function listBuiltinPieceNames(cwd: string, options?: { includeDisabled?: boolean }): string[] {
|
||||
const config = loadConfig(cwd);
|
||||
const lang = config.language;
|
||||
const dir = getBuiltinPiecesDir(lang);
|
||||
const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins();
|
||||
const disabled = options?.includeDisabled ? undefined : (config.disabledBuiltins ?? []);
|
||||
const names = new Set<string>();
|
||||
for (const entry of iteratePieceDir(dir, 'builtin', disabled)) {
|
||||
names.add(entry.name);
|
||||
@ -35,10 +36,11 @@ export function listBuiltinPieceNames(options?: { includeDisabled?: boolean }):
|
||||
}
|
||||
|
||||
/** Get builtin piece by name */
|
||||
export function getBuiltinPiece(name: string, projectCwd?: string): PieceConfig | null {
|
||||
if (!getBuiltinPiecesEnabled()) return null;
|
||||
const lang = getLanguage();
|
||||
const disabled = getDisabledBuiltins();
|
||||
export function getBuiltinPiece(name: string, projectCwd: string): PieceConfig | null {
|
||||
const config = loadConfig(projectCwd);
|
||||
if (config.enableBuiltinPieces === false) return null;
|
||||
const lang = config.language;
|
||||
const disabled = config.disabledBuiltins ?? [];
|
||||
if (disabled.includes(name)) return null;
|
||||
|
||||
const builtinDir = getBuiltinPiecesDir(lang);
|
||||
@ -69,7 +71,7 @@ function resolvePath(pathInput: string, basePath: string): string {
|
||||
function loadPieceFromPath(
|
||||
filePath: string,
|
||||
basePath: string,
|
||||
projectCwd?: string,
|
||||
projectCwd: string,
|
||||
): PieceConfig | null {
|
||||
const resolvedPath = resolvePath(filePath, basePath);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
@ -371,10 +373,11 @@ function* iteratePieceDir(
|
||||
|
||||
/** Get the 3-layer directory list (builtin → user → project-local) */
|
||||
function getPieceDirs(cwd: string): { dir: string; source: PieceSource; disabled?: string[] }[] {
|
||||
const disabled = getDisabledBuiltins();
|
||||
const lang = getLanguage();
|
||||
const config = loadConfig(cwd);
|
||||
const disabled = config.disabledBuiltins ?? [];
|
||||
const lang = config.language;
|
||||
const dirs: { dir: string; source: PieceSource; disabled?: string[] }[] = [];
|
||||
if (getBuiltinPiecesEnabled()) {
|
||||
if (config.enableBuiltinPieces !== false) {
|
||||
dirs.push({ dir: getBuiltinPiecesDir(lang), disabled, source: 'builtin' });
|
||||
}
|
||||
dirs.push({ dir: getGlobalPiecesDir(), source: 'user' });
|
||||
|
||||
@ -27,6 +27,6 @@ function loadEnvBooleanSetting(configKey: string): boolean | undefined {
|
||||
|
||||
export function isVerboseMode(projectDir: string): boolean {
|
||||
const envValue = loadEnvBooleanSetting('verbose');
|
||||
const { project, global } = loadConfig(projectDir);
|
||||
return resolveValue(envValue, project.verbose, global.verbose, false);
|
||||
const config = loadConfig(projectDir);
|
||||
return resolveValue(envValue, undefined, config.verbose, false);
|
||||
}
|
||||
|
||||
22
src/infra/config/resolveConfigValue.ts
Normal file
22
src/infra/config/resolveConfigValue.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { loadConfig, type LoadedConfig } from './loadConfig.js';
|
||||
|
||||
export type ConfigParameterKey = keyof LoadedConfig;
|
||||
|
||||
export function resolveConfigValue<K extends ConfigParameterKey>(
|
||||
projectDir: string,
|
||||
key: K,
|
||||
): LoadedConfig[K] {
|
||||
return loadConfig(projectDir)[key];
|
||||
}
|
||||
|
||||
export function resolveConfigValues<K extends ConfigParameterKey>(
|
||||
projectDir: string,
|
||||
keys: readonly K[],
|
||||
): Pick<LoadedConfig, K> {
|
||||
const config = loadConfig(projectDir);
|
||||
const result = {} as Pick<LoadedConfig, K>;
|
||||
for (const key of keys) {
|
||||
result[key] = config[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -11,7 +11,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createLogger, slugify } from '../../shared/utils/index.js';
|
||||
import { loadConfig } from '../config/index.js';
|
||||
import { resolveConfigValue } from '../config/index.js';
|
||||
import type { WorktreeOptions, WorktreeResult } from './types.js';
|
||||
|
||||
export type { WorktreeOptions, WorktreeResult };
|
||||
@ -36,11 +36,11 @@ export class CloneManager {
|
||||
* Returns the configured worktree_dir (resolved to absolute), or ../
|
||||
*/
|
||||
private static resolveCloneBaseDir(projectDir: string): string {
|
||||
const { global: globalConfig } = loadConfig(projectDir);
|
||||
if (globalConfig.worktreeDir) {
|
||||
return path.isAbsolute(globalConfig.worktreeDir)
|
||||
? globalConfig.worktreeDir
|
||||
: path.resolve(projectDir, globalConfig.worktreeDir);
|
||||
const worktreeDir = resolveConfigValue(projectDir, 'worktreeDir');
|
||||
if (worktreeDir) {
|
||||
return path.isAbsolute(worktreeDir)
|
||||
? worktreeDir
|
||||
: path.resolve(projectDir, worktreeDir);
|
||||
}
|
||||
return path.join(projectDir, '..', 'takt-worktree');
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as wanakana from 'wanakana';
|
||||
import { loadConfig } from '../config/index.js';
|
||||
import { resolveConfigValues } from '../config/index.js';
|
||||
import { getProvider, type ProviderType } from '../providers/index.js';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||
@ -53,7 +53,7 @@ export class TaskSummarizer {
|
||||
taskName: string,
|
||||
options: SummarizeOptions,
|
||||
): Promise<string> {
|
||||
const { global: globalConfig } = loadConfig(options.cwd);
|
||||
const globalConfig = resolveConfigValues(options.cwd, ['branchNameStrategy', 'provider', 'model']);
|
||||
const useLLM = options.useLLM ?? (globalConfig.branchNameStrategy === 'ai');
|
||||
log.info('Summarizing task name', { taskName, useLLM });
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user