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
- 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
- Piece resolver `stat()` calls now catch errors gracefully instead of crashing on inaccessible entries

View File

@ -25,6 +25,8 @@
### Fixed
- オーバーライドピースの検証が ensemble スコープを含むリゾルバー経由で実行されるよう修正
- `takt export-cc` が新しい `builtins/{lang}/facets/` ディレクトリ構造からファセットを読み込むよう修正
- `confirm()` プロンプトがパイプ経由の stdin に対応(例: `echo "y" | takt ensemble add ...`
- イテレーション入力待ち中の `poll_tick` デバッグログ連続出力を抑制
- ピースリゾルバーの `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);
// Persona should be copied to project .takt/personas/
const personaPath = join(repo.path, '.takt', 'personas', 'coder.md');
// Persona should be copied to project .takt/facets/personas/
const personaPath = join(repo.path, '.takt', 'facets', 'personas', 'coder.md');
expect(existsSync(personaPath)).toBe(true);
const content = readFileSync(personaPath, 'utf-8');
expect(content.length).toBeGreaterThan(0);
@ -170,11 +170,11 @@ describe('E2E: Eject builtin pieces (takt eject)', () => {
expect(result.exitCode).toBe(0);
// 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);
// 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);
});

View File

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

View File

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

View File

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

View File

@ -97,6 +97,10 @@ export async function confirm(message: string, defaultYes = true): Promise<boole
const { useTty, forceTouchTty } = resolveTtyPolicy();
assertTtyIfForced(forceTouchTty);
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;
}
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/config-priority.e2e.ts',
'e2e/specs/ensemble.e2e.ts',
'e2e/specs/ensemble-real.e2e.ts',
'e2e/specs/piece-selection-branches.e2e.ts',
],
},
});