fix: update deploySkill for facets layout, add piped stdin confirm support

This commit is contained in:
nrslib 2026-02-22 08:12:00 +09:00
parent 3e9dee5779
commit 53a465ef56
9 changed files with 74 additions and 1101 deletions

View File

@ -25,6 +25,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Fixed ### Fixed
- Override piece validation now includes ensemble scope via the resolver - Override piece validation now includes ensemble scope via the resolver
- `takt export-cc` now reads facets from the new `builtins/{lang}/facets/` directory structure
- `confirm()` prompt now supports piped stdin (e.g., `echo "y" | takt ensemble add ...`)
- Suppressed `poll_tick` debug log flooding during iteration input wait - Suppressed `poll_tick` debug log flooding during iteration input wait
- Piece resolver `stat()` calls now catch errors gracefully instead of crashing on inaccessible entries - Piece resolver `stat()` calls now catch errors gracefully instead of crashing on inaccessible entries

View File

@ -25,6 +25,8 @@
### Fixed ### Fixed
- オーバーライドピースの検証が ensemble スコープを含むリゾルバー経由で実行されるよう修正 - オーバーライドピースの検証が ensemble スコープを含むリゾルバー経由で実行されるよう修正
- `takt export-cc` が新しい `builtins/{lang}/facets/` ディレクトリ構造からファセットを読み込むよう修正
- `confirm()` プロンプトがパイプ経由の stdin に対応(例: `echo "y" | takt ensemble add ...`
- イテレーション入力待ち中の `poll_tick` デバッグログ連続出力を抑制 - イテレーション入力待ち中の `poll_tick` デバッグログ連続出力を抑制
- ピースリゾルバーの `stat()` 呼び出しでアクセス不能エントリ時にクラッシュせずエラーハンドリング - ピースリゾルバーの `stat()` 呼び出しでアクセス不能エントリ時にクラッシュせずエラーハンドリング

File diff suppressed because it is too large Load Diff

View File

@ -153,8 +153,8 @@ describe('E2E: Eject builtin pieces (takt eject)', () => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
// Persona should be copied to project .takt/personas/ // Persona should be copied to project .takt/facets/personas/
const personaPath = join(repo.path, '.takt', 'personas', 'coder.md'); const personaPath = join(repo.path, '.takt', 'facets', 'personas', 'coder.md');
expect(existsSync(personaPath)).toBe(true); expect(existsSync(personaPath)).toBe(true);
const content = readFileSync(personaPath, 'utf-8'); const content = readFileSync(personaPath, 'utf-8');
expect(content.length).toBeGreaterThan(0); expect(content.length).toBeGreaterThan(0);
@ -170,11 +170,11 @@ describe('E2E: Eject builtin pieces (takt eject)', () => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
// Persona should be copied to global dir // Persona should be copied to global dir
const personaPath = join(isolatedEnv.taktDir, 'personas', 'coder.md'); const personaPath = join(isolatedEnv.taktDir, 'facets', 'personas', 'coder.md');
expect(existsSync(personaPath)).toBe(true); expect(existsSync(personaPath)).toBe(true);
// Should NOT be in project dir // Should NOT be in project dir
const projectPersonaPath = join(repo.path, '.takt', 'personas', 'coder.md'); const projectPersonaPath = join(repo.path, '.takt', 'facets', 'personas', 'coder.md');
expect(existsSync(projectPersonaPath)).toBe(false); expect(existsSync(projectPersonaPath)).toBe(false);
}); });

View File

@ -74,20 +74,20 @@ describe('deploySkill', () => {
// Create language-specific directories (en/) // Create language-specific directories (en/)
const langDir = join(fakeResourcesDir, 'en'); const langDir = join(fakeResourcesDir, 'en');
mkdirSync(join(langDir, 'pieces'), { recursive: true }); mkdirSync(join(langDir, 'pieces'), { recursive: true });
mkdirSync(join(langDir, 'personas'), { recursive: true }); mkdirSync(join(langDir, 'facets', 'personas'), { recursive: true });
mkdirSync(join(langDir, 'policies'), { recursive: true }); mkdirSync(join(langDir, 'facets', 'policies'), { recursive: true });
mkdirSync(join(langDir, 'instructions'), { recursive: true }); mkdirSync(join(langDir, 'facets', 'instructions'), { recursive: true });
mkdirSync(join(langDir, 'knowledge'), { recursive: true }); mkdirSync(join(langDir, 'facets', 'knowledge'), { recursive: true });
mkdirSync(join(langDir, 'output-contracts'), { recursive: true }); mkdirSync(join(langDir, 'facets', 'output-contracts'), { recursive: true });
mkdirSync(join(langDir, 'templates'), { recursive: true }); mkdirSync(join(langDir, 'templates'), { recursive: true });
// Add sample files // Add sample files
writeFileSync(join(langDir, 'pieces', 'default.yaml'), 'name: default'); writeFileSync(join(langDir, 'pieces', 'default.yaml'), 'name: default');
writeFileSync(join(langDir, 'personas', 'coder.md'), '# Coder'); writeFileSync(join(langDir, 'facets', 'personas', 'coder.md'), '# Coder');
writeFileSync(join(langDir, 'policies', 'coding.md'), '# Coding'); writeFileSync(join(langDir, 'facets', 'policies', 'coding.md'), '# Coding');
writeFileSync(join(langDir, 'instructions', 'init.md'), '# Init'); writeFileSync(join(langDir, 'facets', 'instructions', 'init.md'), '# Init');
writeFileSync(join(langDir, 'knowledge', 'patterns.md'), '# Patterns'); writeFileSync(join(langDir, 'facets', 'knowledge', 'patterns.md'), '# Patterns');
writeFileSync(join(langDir, 'output-contracts', 'summary.md'), '# Summary'); writeFileSync(join(langDir, 'facets', 'output-contracts', 'summary.md'), '# Summary');
writeFileSync(join(langDir, 'templates', 'task.md'), '# Task'); writeFileSync(join(langDir, 'templates', 'task.md'), '# Task');
// Create target directories // Create target directories

View File

@ -33,16 +33,14 @@ function getSkillDir(): string {
return join(homedir(), '.claude', 'skills', 'takt'); return join(homedir(), '.claude', 'skills', 'takt');
} }
/** Directories within builtins/{lang}/ to copy as resource types */ /** Directories directly under builtins/{lang}/ */
const RESOURCE_DIRS = [ const DIRECT_DIRS = ['pieces', 'templates'] as const;
'pieces',
'personas', /** Facet directories under builtins/{lang}/facets/ */
'policies', const FACET_DIRS = ['personas', 'policies', 'instructions', 'knowledge', 'output-contracts'] as const;
'instructions',
'knowledge', /** All resource directory names (used for summary filtering) */
'output-contracts', const RESOURCE_DIRS = [...DIRECT_DIRS, ...FACET_DIRS] as const;
'templates',
] as const;
/** /**
* Deploy takt skill to Claude Code (~/.claude/). * Deploy takt skill to Claude Code (~/.claude/).
@ -89,10 +87,18 @@ export async function deploySkill(): Promise<void> {
cleanDir(refsDestDir); cleanDir(refsDestDir);
copyDirRecursive(refsSrcDir, refsDestDir, copiedFiles); copyDirRecursive(refsSrcDir, refsDestDir, copiedFiles);
// 3. Deploy all resource directories from builtins/{lang}/ // 3. Deploy direct resource directories from builtins/{lang}/
for (const resourceDir of RESOURCE_DIRS) { for (const dir of DIRECT_DIRS) {
const srcDir = join(langResourcesDir, resourceDir); const srcDir = join(langResourcesDir, dir);
const destDir = join(skillDir, resourceDir); const destDir = join(skillDir, dir);
cleanDir(destDir);
copyDirRecursive(srcDir, destDir, copiedFiles);
}
// 4. Deploy facet directories from builtins/{lang}/facets/
for (const dir of FACET_DIRS) {
const srcDir = join(langResourcesDir, 'facets', dir);
const destDir = join(skillDir, dir);
cleanDir(destDir); cleanDir(destDir);
copyDirRecursive(srcDir, destDir, copiedFiles); copyDirRecursive(srcDir, destDir, copiedFiles);
} }

View File

@ -109,9 +109,9 @@ export function isVersionCompatible(minVersion: string, currentVersion: string):
* Throws if neither exists (empty package). * Throws if neither exists (empty package).
*/ */
export function checkPackageHasContent(packageRoot: string): void { export function checkPackageHasContent(packageRoot: string): void {
const hasFaceted = existsSync(join(packageRoot, 'facets')); const hasFacets = existsSync(join(packageRoot, 'facets'));
const hasPieces = existsSync(join(packageRoot, 'pieces')); const hasPieces = existsSync(join(packageRoot, 'pieces'));
if (!hasFaceted && !hasPieces) { if (!hasFacets && !hasPieces) {
throw new Error( throw new Error(
`Package at "${packageRoot}" has neither facets/ nor pieces/ directory — empty package rejected`, `Package at "${packageRoot}" has neither facets/ nor pieces/ directory — empty package rejected`,
); );
@ -132,7 +132,7 @@ export function checkPackageHasContentWithContext(
const hasPieces = existsSync(join(packageRoot, 'pieces')); const hasPieces = existsSync(join(packageRoot, 'pieces'));
if (hasFacets || hasPieces) return; if (hasFacets || hasPieces) return;
const checkedFaceted = join(packageRoot, 'facets'); const checkedFacets = join(packageRoot, 'facets');
const checkedPieces = join(packageRoot, 'pieces'); const checkedPieces = join(packageRoot, 'pieces');
const configuredPath = context.configuredPath ?? '.'; const configuredPath = context.configuredPath ?? '.';
const manifestPath = context.manifestPath ?? '(unknown)'; const manifestPath = context.manifestPath ?? '(unknown)';
@ -146,7 +146,7 @@ export function checkPackageHasContentWithContext(
`manifest: ${manifestPath}`, `manifest: ${manifestPath}`,
`configured path: ${configuredPath}`, `configured path: ${configuredPath}`,
`resolved package root: ${packageRoot}`, `resolved package root: ${packageRoot}`,
`checked: ${checkedFaceted}`, `checked: ${checkedFacets}`,
`checked: ${checkedPieces}`, `checked: ${checkedPieces}`,
hint, hint,
].join('\n'), ].join('\n'),

View File

@ -97,6 +97,10 @@ export async function confirm(message: string, defaultYes = true): Promise<boole
const { useTty, forceTouchTty } = resolveTtyPolicy(); const { useTty, forceTouchTty } = resolveTtyPolicy();
assertTtyIfForced(forceTouchTty); assertTtyIfForced(forceTouchTty);
if (!useTty) { if (!useTty) {
// Support piped stdin (e.g. echo "y" | takt ensemble add ...)
if (!process.stdin.isTTY && process.stdin.readable && !process.stdin.destroyed) {
return readConfirmFromPipe(defaultYes);
}
return defaultYes; return defaultYes;
} }
const rl = readline.createInterface({ const rl = readline.createInterface({
@ -122,3 +126,29 @@ export async function confirm(message: string, defaultYes = true): Promise<boole
}); });
}); });
} }
function readConfirmFromPipe(defaultYes: boolean): Promise<boolean> {
const rl = readline.createInterface({ input: process.stdin });
return new Promise((resolve) => {
let resolved = false;
rl.once('line', (line) => {
resolved = true;
rl.close();
pauseStdinSafely();
const trimmed = line.trim().toLowerCase();
if (!trimmed) {
resolve(defaultYes);
return;
}
resolve(trimmed === 'y' || trimmed === 'yes');
});
rl.once('close', () => {
if (!resolved) {
resolve(defaultYes);
}
});
});
}

View File

@ -37,6 +37,8 @@ export default defineConfig({
'e2e/specs/task-content-file.e2e.ts', 'e2e/specs/task-content-file.e2e.ts',
'e2e/specs/config-priority.e2e.ts', 'e2e/specs/config-priority.e2e.ts',
'e2e/specs/ensemble.e2e.ts', 'e2e/specs/ensemble.e2e.ts',
'e2e/specs/ensemble-real.e2e.ts',
'e2e/specs/piece-selection-branches.e2e.ts',
], ],
}, },
}); });