feat(runtime): add configurable prepare presets and provider e2e
This commit is contained in:
parent
18bad35489
commit
6e14cd3c38
@ -45,6 +45,12 @@ provider: claude
|
|||||||
# Prevent macOS idle sleep during execution using caffeinate (default: false)
|
# Prevent macOS idle sleep during execution using caffeinate (default: false)
|
||||||
# prevent_sleep: false
|
# prevent_sleep: false
|
||||||
|
|
||||||
|
# Runtime environment defaults (applies to all pieces unless piece_config.runtime overrides)
|
||||||
|
# runtime:
|
||||||
|
# prepare:
|
||||||
|
# - gradle
|
||||||
|
# - node
|
||||||
|
|
||||||
# ── Parallel Execution (takt run) ──
|
# ── Parallel Execution (takt run) ──
|
||||||
|
|
||||||
# Number of tasks to run concurrently (1 = sequential, max: 10)
|
# Number of tasks to run concurrently (1 = sequential, max: 10)
|
||||||
|
|||||||
@ -45,6 +45,12 @@ provider: claude
|
|||||||
# macOS のアイドルスリープを防止(デフォルト: false)
|
# macOS のアイドルスリープを防止(デフォルト: false)
|
||||||
# prevent_sleep: false
|
# prevent_sleep: false
|
||||||
|
|
||||||
|
# 実行時ランタイム環境のデフォルト(piece_config.runtime があればそちらを優先)
|
||||||
|
# runtime:
|
||||||
|
# prepare:
|
||||||
|
# - gradle
|
||||||
|
# - node
|
||||||
|
|
||||||
# ── 並列実行 (takt run) ──
|
# ── 並列実行 (takt run) ──
|
||||||
|
|
||||||
# タスクの同時実行数(1 = 逐次実行、最大: 10)
|
# タスクの同時実行数(1 = 逐次実行、最大: 10)
|
||||||
|
|||||||
@ -113,6 +113,14 @@ E2Eテストを追加・変更した場合は、このドキュメントも更
|
|||||||
- `takt run --provider mock` を起動し、`=== Running Piece:` が出たら `Ctrl+C` を送る。
|
- `takt run --provider mock` を起動し、`=== Running Piece:` が出たら `Ctrl+C` を送る。
|
||||||
- 3件目タスク(`sigint-c`)が開始されないことを確認する。
|
- 3件目タスク(`sigint-c`)が開始されないことを確認する。
|
||||||
- `=== Tasks Summary ===` 以降に新規タスク開始やクローン作成ログが出ないことを確認する。
|
- `=== Tasks Summary ===` 以降に新規タスク開始やクローン作成ログが出ないことを確認する。
|
||||||
|
- Runtime config injection with provider(`e2e/specs/runtime-config-provider.e2e.ts`)
|
||||||
|
- 目的: `config.yaml` の `runtime.prepare` が provider 実行時に反映される正例と、未設定時の失敗再現(env未注入)を確認。
|
||||||
|
- LLM: 条件付き(`TAKT_E2E_PROVIDER` が `claude` / `codex` / `opencode` の場合に実行、未指定時は skip)
|
||||||
|
- 手順(ユーザー行動/コマンド):
|
||||||
|
- E2E用 `config.yaml` に `runtime.prepare: [gradle, node]` を設定する。
|
||||||
|
- `takt --task '<gradle/npm を実行する指示>' --piece e2e/fixtures/pieces/simple.yaml --create-worktree no` を実行する。
|
||||||
|
- 正例では、作業リポジトリに `.runtime/env.sh` と `.runtime/{tmp,cache,config,state,gradle,npm}` が作成されていることを確認する。
|
||||||
|
- 負例(`runtime.prepare` 未設定)では、`GRADLE_USER_HOME is required` と npm キャッシュ書き込み失敗が出力され、`.runtime/env.sh` が生成されないことを確認する。
|
||||||
- List tasks non-interactive(`e2e/specs/list-non-interactive.e2e.ts`)
|
- List tasks non-interactive(`e2e/specs/list-non-interactive.e2e.ts`)
|
||||||
- 目的: `takt list` の非対話モードでブランチ操作ができることを確認。
|
- 目的: `takt list` の非対話モードでブランチ操作ができることを確認。
|
||||||
- LLM: 呼び出さない(LLM不使用の操作のみ)
|
- LLM: 呼び出さない(LLM不使用の操作のみ)
|
||||||
|
|||||||
168
e2e/specs/runtime-config-provider.e2e.ts
Normal file
168
e2e/specs/runtime-config-provider.e2e.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
||||||
|
import { join, resolve, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { createIsolatedEnv, type IsolatedEnv, updateIsolatedConfig } from '../helpers/isolated-env';
|
||||||
|
import { runTakt } from '../helpers/takt-runner';
|
||||||
|
import { createLocalRepo, type LocalRepo } from '../helpers/test-repo';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const provider = process.env.TAKT_E2E_PROVIDER;
|
||||||
|
const providerEnabled = provider != null && provider !== 'mock';
|
||||||
|
const providerIt = providerEnabled ? it : it.skip;
|
||||||
|
|
||||||
|
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||||
|
describe('E2E: runtime.prepare with provider', () => {
|
||||||
|
let isolatedEnv: IsolatedEnv;
|
||||||
|
let repo: LocalRepo;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
isolatedEnv = createIsolatedEnv();
|
||||||
|
repo = createLocalRepo();
|
||||||
|
mkdirSync(join(repo.path, 'scripts'), { recursive: true });
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(repo.path, 'gradlew'),
|
||||||
|
[
|
||||||
|
'#!/usr/bin/env bash',
|
||||||
|
'set -euo pipefail',
|
||||||
|
'if [ -z "${GRADLE_USER_HOME:-}" ]; then echo "GRADLE_USER_HOME is required"; exit 2; fi',
|
||||||
|
'if [ -z "${TMPDIR:-}" ]; then echo "TMPDIR is required"; exit 3; fi',
|
||||||
|
'mkdir -p "$GRADLE_USER_HOME"',
|
||||||
|
'mkdir -p "$TMPDIR"',
|
||||||
|
'echo "ok" > "$GRADLE_USER_HOME/gradle-ok.txt"',
|
||||||
|
'echo "ok" > "$TMPDIR/gradle-tmp-ok.txt"',
|
||||||
|
'echo "BUILD SUCCESSFUL"',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
chmodSync(join(repo.path, 'gradlew'), 0o755);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(repo.path, 'scripts/check-node-env.js'),
|
||||||
|
[
|
||||||
|
"const fs = require('node:fs');",
|
||||||
|
"const path = require('node:path');",
|
||||||
|
"const cache = process.env.npm_config_cache;",
|
||||||
|
"if (!cache) { console.error('npm_config_cache is required'); process.exit(2); }",
|
||||||
|
"fs.mkdirSync(cache, { recursive: true });",
|
||||||
|
"fs.writeFileSync(path.join(cache, 'npm-ok.txt'), 'ok');",
|
||||||
|
"console.log('node-env-ok');",
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(repo.path, 'package.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'runtime-e2e',
|
||||||
|
private: true,
|
||||||
|
version: '1.0.0',
|
||||||
|
scripts: {
|
||||||
|
test: 'node scripts/check-node-env.js',
|
||||||
|
},
|
||||||
|
}, null, 2),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(repo.path, 'runtime-e2e-piece.yaml'),
|
||||||
|
[
|
||||||
|
'name: runtime-e2e',
|
||||||
|
'description: Runtime env injection verification piece',
|
||||||
|
'max_movements: 3',
|
||||||
|
'initial_movement: execute',
|
||||||
|
'movements:',
|
||||||
|
' - name: execute',
|
||||||
|
' edit: false',
|
||||||
|
' persona: ../fixtures/agents/test-coder.md',
|
||||||
|
' allowed_tools:',
|
||||||
|
' - Read',
|
||||||
|
' - Bash',
|
||||||
|
' permission_mode: edit',
|
||||||
|
' instruction_template: |',
|
||||||
|
' {task}',
|
||||||
|
' rules:',
|
||||||
|
' - condition: Task completed',
|
||||||
|
' next: COMPLETE',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
try { repo.cleanup(); } catch { /* best-effort */ }
|
||||||
|
try { isolatedEnv.cleanup(); } catch { /* best-effort */ }
|
||||||
|
});
|
||||||
|
|
||||||
|
providerIt('should apply runtime.prepare from config.yaml during provider execution', () => {
|
||||||
|
updateIsolatedConfig(isolatedEnv.taktDir, {
|
||||||
|
runtime: {
|
||||||
|
prepare: ['gradle', 'node'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const piecePath = join(repo.path, 'runtime-e2e-piece.yaml');
|
||||||
|
const result = runTakt({
|
||||||
|
args: [
|
||||||
|
'--task',
|
||||||
|
[
|
||||||
|
'Run `./gradlew test` and `npm test` in the repository root.',
|
||||||
|
'If both commands succeed, respond exactly with: Task completed',
|
||||||
|
].join(' '),
|
||||||
|
'--piece', piecePath,
|
||||||
|
'--create-worktree', 'no',
|
||||||
|
],
|
||||||
|
cwd: repo.path,
|
||||||
|
env: isolatedEnv.env,
|
||||||
|
timeout: 240_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
|
||||||
|
const runtimeRoot = join(repo.path, '.runtime');
|
||||||
|
const envFile = join(runtimeRoot, 'env.sh');
|
||||||
|
expect(existsSync(runtimeRoot)).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'tmp'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'cache'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'config'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'state'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'gradle'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'npm'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'gradle', 'gradle-ok.txt'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'npm', 'npm-ok.txt'))).toBe(true);
|
||||||
|
expect(existsSync(envFile)).toBe(true);
|
||||||
|
|
||||||
|
const envContent = readFileSync(envFile, 'utf-8');
|
||||||
|
expect(envContent).toContain('export TMPDIR=');
|
||||||
|
expect(envContent).toContain('export GRADLE_USER_HOME=');
|
||||||
|
expect(envContent).toContain('export npm_config_cache=');
|
||||||
|
}, 240_000);
|
||||||
|
|
||||||
|
providerIt('should reproduce missing runtime env failure when runtime.prepare is unset', () => {
|
||||||
|
const piecePath = join(repo.path, 'runtime-e2e-piece.yaml');
|
||||||
|
const result = runTakt({
|
||||||
|
args: [
|
||||||
|
'--task',
|
||||||
|
[
|
||||||
|
'Run `./gradlew test` and `npm test` in the repository root without setting or overriding environment variables.',
|
||||||
|
'If both commands succeed, respond exactly with: Task completed',
|
||||||
|
].join(' '),
|
||||||
|
'--piece', piecePath,
|
||||||
|
'--create-worktree', 'no',
|
||||||
|
],
|
||||||
|
cwd: repo.path,
|
||||||
|
env: isolatedEnv.env,
|
||||||
|
timeout: 240_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const combined = `${result.stdout}\n${result.stderr}`;
|
||||||
|
|
||||||
|
expect(combined).toContain('GRADLE_USER_HOME is required');
|
||||||
|
|
||||||
|
const runtimeRoot = join(repo.path, '.runtime');
|
||||||
|
expect(existsSync(join(runtimeRoot, 'env.sh'))).toBe(false);
|
||||||
|
}, 240_000);
|
||||||
|
});
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"takt-cli": "./dist/app/cli/index.js"
|
"takt-cli": "./dist/app/cli/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && mkdir -p dist/shared/prompts/en dist/shared/prompts/ja dist/shared/i18n && cp src/shared/prompts/en/*.md dist/shared/prompts/en/ && cp src/shared/prompts/ja/*.md dist/shared/prompts/ja/ && cp src/shared/i18n/labels_en.yaml src/shared/i18n/labels_ja.yaml dist/shared/i18n/",
|
"build": "tsc && mkdir -p dist/shared/prompts/en dist/shared/prompts/ja dist/shared/i18n dist/core/runtime/presets && cp src/shared/prompts/en/*.md dist/shared/prompts/en/ && cp src/shared/prompts/ja/*.md dist/shared/prompts/ja/ && cp src/shared/i18n/labels_en.yaml src/shared/i18n/labels_ja.yaml dist/shared/i18n/ && cp src/core/runtime/presets/*.sh dist/core/runtime/presets/",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
|||||||
@ -483,6 +483,41 @@ describe('loadGlobalConfig', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('runtime', () => {
|
||||||
|
it('should load runtime.prepare from config.yaml', () => {
|
||||||
|
const taktDir = join(testHomeDir, '.takt');
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
getGlobalConfigPath(),
|
||||||
|
[
|
||||||
|
'language: en',
|
||||||
|
'runtime:',
|
||||||
|
' prepare:',
|
||||||
|
' - gradle',
|
||||||
|
' - node',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = loadGlobalConfig();
|
||||||
|
expect(config.runtime).toEqual({ prepare: ['gradle', 'node'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save and reload runtime.prepare', () => {
|
||||||
|
const taktDir = join(testHomeDir, '.takt');
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8');
|
||||||
|
|
||||||
|
const config = loadGlobalConfig();
|
||||||
|
config.runtime = { prepare: ['gradle', 'node'] };
|
||||||
|
saveGlobalConfig(config);
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
|
||||||
|
const reloaded = loadGlobalConfig();
|
||||||
|
expect(reloaded.runtime).toEqual({ prepare: ['gradle', 'node'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('provider/model compatibility validation', () => {
|
describe('provider/model compatibility validation', () => {
|
||||||
it('should throw when provider is codex but model is a Claude alias (opus)', () => {
|
it('should throw when provider is codex but model is a Claude alias (opus)', () => {
|
||||||
const taktDir = join(testHomeDir, '.takt');
|
const taktDir = join(testHomeDir, '.takt');
|
||||||
|
|||||||
@ -153,6 +153,30 @@ describe('PieceConfigRawSchema', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse piece-level piece_config.runtime.prepare', () => {
|
||||||
|
const config = {
|
||||||
|
name: 'test-piece',
|
||||||
|
piece_config: {
|
||||||
|
runtime: {
|
||||||
|
prepare: ['gradle', 'node'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
movements: [
|
||||||
|
{
|
||||||
|
name: 'implement',
|
||||||
|
instruction: '{task}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = PieceConfigRawSchema.parse(config);
|
||||||
|
expect(result.piece_config).toEqual({
|
||||||
|
runtime: {
|
||||||
|
prepare: ['gradle', 'node'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow omitting permission_mode', () => {
|
it('should allow omitting permission_mode', () => {
|
||||||
const config = {
|
const config = {
|
||||||
name: 'test-piece',
|
name: 'test-piece',
|
||||||
|
|||||||
@ -91,6 +91,29 @@ describe('normalizePieceConfig provider_options', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('piece-level runtime.prepare を正規化し重複を除去する', () => {
|
||||||
|
const raw = {
|
||||||
|
name: 'runtime-prepare',
|
||||||
|
piece_config: {
|
||||||
|
runtime: {
|
||||||
|
prepare: ['gradle', 'node', 'gradle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
movements: [
|
||||||
|
{
|
||||||
|
name: 'implement',
|
||||||
|
instruction: '{task}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = normalizePieceConfig(raw, process.cwd());
|
||||||
|
|
||||||
|
expect(config.runtime).toEqual({
|
||||||
|
prepare: ['gradle', 'node'],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mergeProviderOptions', () => {
|
describe('mergeProviderOptions', () => {
|
||||||
|
|||||||
113
src/__tests__/runtime-environment.test.ts
Normal file
113
src/__tests__/runtime-environment.test.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import { prepareRuntimeEnvironment, resolveRuntimeConfig } from '../core/runtime/runtime-environment.js';
|
||||||
|
|
||||||
|
describe('prepareRuntimeEnvironment', () => {
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
const systemTmpDir = tmpdir();
|
||||||
|
const envKeys = [
|
||||||
|
'TMPDIR',
|
||||||
|
'XDG_CACHE_HOME',
|
||||||
|
'XDG_CONFIG_HOME',
|
||||||
|
'XDG_STATE_HOME',
|
||||||
|
'CI',
|
||||||
|
'JAVA_TOOL_OPTIONS',
|
||||||
|
'GRADLE_USER_HOME',
|
||||||
|
'npm_config_cache',
|
||||||
|
] as const;
|
||||||
|
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
for (const key of envKeys) {
|
||||||
|
const value = originalEnv[key];
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when runtime.prepare is not set', () => {
|
||||||
|
const cwd = mkdtempSync(join(systemTmpDir, 'takt-runtime-env-'));
|
||||||
|
tempDirs.push(cwd);
|
||||||
|
|
||||||
|
const result = prepareRuntimeEnvironment(cwd, undefined);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create .runtime files and inject tool-specific env', () => {
|
||||||
|
const cwd = mkdtempSync(join(systemTmpDir, 'takt-runtime-env-'));
|
||||||
|
tempDirs.push(cwd);
|
||||||
|
|
||||||
|
const result = prepareRuntimeEnvironment(cwd, {
|
||||||
|
prepare: ['gradle', 'node'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.prepare).toEqual(['gradle', 'node']);
|
||||||
|
|
||||||
|
const runtimeRoot = join(cwd, '.runtime');
|
||||||
|
expect(existsSync(runtimeRoot)).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'tmp'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'cache'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'config'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'state'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'gradle'))).toBe(true);
|
||||||
|
expect(existsSync(join(runtimeRoot, 'npm'))).toBe(true);
|
||||||
|
|
||||||
|
const envFile = join(runtimeRoot, 'env.sh');
|
||||||
|
expect(existsSync(envFile)).toBe(true);
|
||||||
|
const envContent = readFileSync(envFile, 'utf-8');
|
||||||
|
expect(envContent).toContain('export TMPDIR=');
|
||||||
|
expect(envContent).toContain('export GRADLE_USER_HOME=');
|
||||||
|
expect(envContent).toContain('export npm_config_cache=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute custom prepare script path and merge exported env', () => {
|
||||||
|
const cwd = mkdtempSync(join(systemTmpDir, 'takt-runtime-env-'));
|
||||||
|
tempDirs.push(cwd);
|
||||||
|
|
||||||
|
const scriptPath = join(cwd, 'prepare-custom.sh');
|
||||||
|
writeFileSync(scriptPath, [
|
||||||
|
'#!/usr/bin/env bash',
|
||||||
|
'set -euo pipefail',
|
||||||
|
'runtime_root="${TAKT_RUNTIME_ROOT:?}"',
|
||||||
|
'custom_dir="$runtime_root/custom-cache"',
|
||||||
|
'mkdir -p "$custom_dir"',
|
||||||
|
'echo "CUSTOM_CACHE_DIR=$custom_dir"',
|
||||||
|
].join('\n'), 'utf-8');
|
||||||
|
chmodSync(scriptPath, 0o755);
|
||||||
|
|
||||||
|
const result = prepareRuntimeEnvironment(cwd, {
|
||||||
|
prepare: [scriptPath],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.injectedEnv.CUSTOM_CACHE_DIR).toBe(join(cwd, '.runtime', 'custom-cache'));
|
||||||
|
expect(existsSync(join(cwd, '.runtime', 'custom-cache'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveRuntimeConfig', () => {
|
||||||
|
it('should use piece runtime when both global and piece are defined', () => {
|
||||||
|
const resolved = resolveRuntimeConfig(
|
||||||
|
{ prepare: ['gradle', 'node'] },
|
||||||
|
{ prepare: ['node', 'pnpm'] },
|
||||||
|
);
|
||||||
|
expect(resolved).toEqual({ prepare: ['node', 'pnpm'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to global runtime when piece runtime is missing', () => {
|
||||||
|
const resolved = resolveRuntimeConfig(
|
||||||
|
{ prepare: ['gradle', 'node', 'gradle'] },
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(resolved).toEqual({ prepare: ['gradle', 'node'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -2,7 +2,7 @@
|
|||||||
* Configuration types (global and project)
|
* Configuration types (global and project)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MovementProviderOptions } from './piece-types.js';
|
import type { MovementProviderOptions, PieceRuntimeConfig } from './piece-types.js';
|
||||||
|
|
||||||
/** Custom agent configuration */
|
/** Custom agent configuration */
|
||||||
export interface CustomAgentConfig {
|
export interface CustomAgentConfig {
|
||||||
@ -90,6 +90,8 @@ export interface GlobalConfig {
|
|||||||
personaProviders?: Record<string, 'claude' | 'codex' | 'opencode' | 'mock'>;
|
personaProviders?: Record<string, 'claude' | 'codex' | 'opencode' | 'mock'>;
|
||||||
/** Global provider-specific options (lowest priority) */
|
/** Global provider-specific options (lowest priority) */
|
||||||
providerOptions?: MovementProviderOptions;
|
providerOptions?: MovementProviderOptions;
|
||||||
|
/** Global runtime environment defaults (can be overridden by piece runtime) */
|
||||||
|
runtime?: PieceRuntimeConfig;
|
||||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
branchNameStrategy?: 'romaji' | 'ai';
|
branchNameStrategy?: 'romaji' | 'ai';
|
||||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||||
|
|||||||
@ -8,6 +8,9 @@ export type {
|
|||||||
OutputContractItem,
|
OutputContractItem,
|
||||||
OutputContractEntry,
|
OutputContractEntry,
|
||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
|
RuntimePreparePreset,
|
||||||
|
RuntimePrepareEntry,
|
||||||
|
PieceRuntimeConfig,
|
||||||
AgentResponse,
|
AgentResponse,
|
||||||
SessionState,
|
SessionState,
|
||||||
PartDefinition,
|
PartDefinition,
|
||||||
|
|||||||
@ -92,6 +92,17 @@ export interface OpenCodeProviderOptions {
|
|||||||
networkAccess?: boolean;
|
networkAccess?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Runtime prepare preset identifiers */
|
||||||
|
export type RuntimePreparePreset = 'gradle' | 'node';
|
||||||
|
/** Runtime prepare entry: preset name or executable script path */
|
||||||
|
export type RuntimePrepareEntry = RuntimePreparePreset | string;
|
||||||
|
|
||||||
|
/** Piece-level runtime environment settings */
|
||||||
|
export interface PieceRuntimeConfig {
|
||||||
|
/** Preset(s) or script path(s) to prepare .runtime and inject env vars */
|
||||||
|
prepare?: RuntimePrepareEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
/** Claude sandbox settings (maps to SDK SandboxSettings) */
|
/** Claude sandbox settings (maps to SDK SandboxSettings) */
|
||||||
export interface ClaudeSandboxSettings {
|
export interface ClaudeSandboxSettings {
|
||||||
/** Allow all Bash commands to run outside the sandbox */
|
/** Allow all Bash commands to run outside the sandbox */
|
||||||
@ -237,6 +248,8 @@ export interface PieceConfig {
|
|||||||
description?: string;
|
description?: string;
|
||||||
/** Piece-level default provider options (used as movement defaults) */
|
/** Piece-level default provider options (used as movement defaults) */
|
||||||
providerOptions?: MovementProviderOptions;
|
providerOptions?: MovementProviderOptions;
|
||||||
|
/** Piece-level runtime preparation and env injection settings */
|
||||||
|
runtime?: PieceRuntimeConfig;
|
||||||
/** Persona definitions — map of name to file path or inline content (raw, not content-resolved) */
|
/** Persona definitions — map of name to file path or inline content (raw, not content-resolved) */
|
||||||
personas?: Record<string, string>;
|
personas?: Record<string, string>;
|
||||||
/** Resolved policy definitions — map of name to file content (resolved at parse time) */
|
/** Resolved policy definitions — map of name to file content (resolved at parse time) */
|
||||||
|
|||||||
@ -78,9 +78,23 @@ export const MovementProviderOptionsSchema = z.object({
|
|||||||
}).optional(),
|
}).optional(),
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|
||||||
|
/** Runtime prepare preset identifiers */
|
||||||
|
export const RuntimePreparePresetSchema = z.enum(['gradle', 'node']);
|
||||||
|
/** Runtime prepare entry: preset name or script path */
|
||||||
|
export const RuntimePrepareEntrySchema = z.union([
|
||||||
|
RuntimePreparePresetSchema,
|
||||||
|
z.string().min(1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Piece-level runtime settings */
|
||||||
|
export const RuntimeConfigSchema = z.object({
|
||||||
|
prepare: z.array(RuntimePrepareEntrySchema).optional(),
|
||||||
|
}).optional();
|
||||||
|
|
||||||
/** Piece-level provider options schema */
|
/** Piece-level provider options schema */
|
||||||
export const PieceProviderOptionsSchema = z.object({
|
export const PieceProviderOptionsSchema = z.object({
|
||||||
provider_options: MovementProviderOptionsSchema,
|
provider_options: MovementProviderOptionsSchema,
|
||||||
|
runtime: RuntimeConfigSchema,
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -425,6 +439,8 @@ export const GlobalConfigSchema = z.object({
|
|||||||
persona_providers: z.record(z.string(), z.enum(['claude', 'codex', 'opencode', 'mock'])).optional(),
|
persona_providers: z.record(z.string(), z.enum(['claude', 'codex', 'opencode', 'mock'])).optional(),
|
||||||
/** Global provider-specific options (lowest priority) */
|
/** Global provider-specific options (lowest priority) */
|
||||||
provider_options: MovementProviderOptionsSchema,
|
provider_options: MovementProviderOptionsSchema,
|
||||||
|
/** Global runtime defaults (piece runtime overrides this) */
|
||||||
|
runtime: RuntimeConfigSchema,
|
||||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||||
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
||||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||||
|
|||||||
@ -37,6 +37,9 @@ export type {
|
|||||||
OutputContractItem,
|
OutputContractItem,
|
||||||
OutputContractEntry,
|
OutputContractEntry,
|
||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
|
RuntimePreparePreset,
|
||||||
|
RuntimePrepareEntry,
|
||||||
|
PieceRuntimeConfig,
|
||||||
MovementProviderOptions,
|
MovementProviderOptions,
|
||||||
PieceMovement,
|
PieceMovement,
|
||||||
ArpeggioMovementConfig,
|
ArpeggioMovementConfig,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { ParallelRunner } from './ParallelRunner.js';
|
|||||||
import { ArpeggioRunner } from './ArpeggioRunner.js';
|
import { ArpeggioRunner } from './ArpeggioRunner.js';
|
||||||
import { TeamLeaderRunner } from './TeamLeaderRunner.js';
|
import { TeamLeaderRunner } from './TeamLeaderRunner.js';
|
||||||
import { buildRunPaths, type RunPaths } from '../run/run-paths.js';
|
import { buildRunPaths, type RunPaths } from '../run/run-paths.js';
|
||||||
|
import { prepareRuntimeEnvironment } from '../../runtime/runtime-environment.js';
|
||||||
|
|
||||||
const log = createLogger('engine');
|
const log = createLogger('engine');
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ export class PieceEngine extends EventEmitter {
|
|||||||
this.runPaths = buildRunPaths(this.cwd, reportDirName);
|
this.runPaths = buildRunPaths(this.cwd, reportDirName);
|
||||||
this.reportDir = this.runPaths.reportsRel;
|
this.reportDir = this.runPaths.reportsRel;
|
||||||
this.ensureRunDirsExist();
|
this.ensureRunDirsExist();
|
||||||
|
this.applyRuntimeEnvironment('init');
|
||||||
this.validateConfig();
|
this.validateConfig();
|
||||||
this.state = createInitialState(config, options);
|
this.state = createInitialState(config, options);
|
||||||
this.detectRuleIndex = options.detectRuleIndex ?? (() => {
|
this.detectRuleIndex = options.detectRuleIndex ?? (() => {
|
||||||
@ -215,6 +217,20 @@ export class PieceEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyRuntimeEnvironment(stage: 'init' | 'movement'): void {
|
||||||
|
const prepared = prepareRuntimeEnvironment(this.cwd, this.config.runtime);
|
||||||
|
if (!prepared) return;
|
||||||
|
log.info('Runtime environment prepared', {
|
||||||
|
stage,
|
||||||
|
runtimeRoot: prepared.runtimeRoot,
|
||||||
|
envFile: prepared.envFile,
|
||||||
|
prepare: prepared.prepare,
|
||||||
|
tmpdir: prepared.injectedEnv.TMPDIR,
|
||||||
|
gradleUserHome: prepared.injectedEnv.GRADLE_USER_HOME,
|
||||||
|
npmCache: prepared.injectedEnv.npm_config_cache,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Validate piece configuration at construction time */
|
/** Validate piece configuration at construction time */
|
||||||
private validateConfig(): void {
|
private validateConfig(): void {
|
||||||
const initialMovement = this.config.movements.find((s) => s.name === this.config.initialMovement);
|
const initialMovement = this.config.movements.find((s) => s.name === this.config.initialMovement);
|
||||||
@ -538,6 +554,7 @@ export class PieceEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const movement = this.getMovement(this.state.currentMovement);
|
const movement = this.getMovement(this.state.currentMovement);
|
||||||
|
this.applyRuntimeEnvironment('movement');
|
||||||
const loopCheck = this.loopDetector.check(movement.name);
|
const loopCheck = this.loopDetector.check(movement.name);
|
||||||
|
|
||||||
if (loopCheck.shouldWarn) {
|
if (loopCheck.shouldWarn) {
|
||||||
@ -676,6 +693,7 @@ export class PieceEngine extends EventEmitter {
|
|||||||
loopDetected?: boolean;
|
loopDetected?: boolean;
|
||||||
}> {
|
}> {
|
||||||
const movement = this.getMovement(this.state.currentMovement);
|
const movement = this.getMovement(this.state.currentMovement);
|
||||||
|
this.applyRuntimeEnvironment('movement');
|
||||||
const loopCheck = this.loopDetector.check(movement.name);
|
const loopCheck = this.loopDetector.check(movement.name);
|
||||||
|
|
||||||
if (loopCheck.shouldAbort) {
|
if (loopCheck.shouldAbort) {
|
||||||
|
|||||||
11
src/core/runtime/presets/prepare-gradle.sh
Executable file
11
src/core/runtime/presets/prepare-gradle.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
runtime_root="${TAKT_RUNTIME_ROOT:?TAKT_RUNTIME_ROOT is required}"
|
||||||
|
runtime_tmp="${TAKT_RUNTIME_TMP:-$runtime_root/tmp}"
|
||||||
|
gradle_home="$runtime_root/gradle"
|
||||||
|
|
||||||
|
mkdir -p "$runtime_tmp" "$gradle_home"
|
||||||
|
|
||||||
|
echo "GRADLE_USER_HOME=$gradle_home"
|
||||||
|
echo "TMPDIR=$runtime_tmp"
|
||||||
11
src/core/runtime/presets/prepare-node.sh
Executable file
11
src/core/runtime/presets/prepare-node.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
runtime_root="${TAKT_RUNTIME_ROOT:?TAKT_RUNTIME_ROOT is required}"
|
||||||
|
runtime_tmp="${TAKT_RUNTIME_TMP:-$runtime_root/tmp}"
|
||||||
|
npm_cache="$runtime_root/npm"
|
||||||
|
|
||||||
|
mkdir -p "$runtime_tmp" "$npm_cache"
|
||||||
|
|
||||||
|
echo "npm_config_cache=$npm_cache"
|
||||||
|
echo "TMPDIR=$runtime_tmp"
|
||||||
208
src/core/runtime/runtime-environment.ts
Normal file
208
src/core/runtime/runtime-environment.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
|
import { join, resolve, isAbsolute, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import type { PieceRuntimeConfig, RuntimePrepareEntry, RuntimePreparePreset } from '../models/piece-types.js';
|
||||||
|
|
||||||
|
export interface RuntimeEnvironmentResult {
|
||||||
|
runtimeRoot: string;
|
||||||
|
envFile: string;
|
||||||
|
prepare: RuntimePrepareEntry[];
|
||||||
|
injectedEnv: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const PRESET_SCRIPT_DIR = join(__dirname, 'presets');
|
||||||
|
const PRESET_SCRIPT_MAP: Record<RuntimePreparePreset, string> = {
|
||||||
|
gradle: join(PRESET_SCRIPT_DIR, 'prepare-gradle.sh'),
|
||||||
|
node: join(PRESET_SCRIPT_DIR, 'prepare-node.sh'),
|
||||||
|
};
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseEnvironment(runtimeRoot: string): Record<string, string> {
|
||||||
|
return {
|
||||||
|
TMPDIR: join(runtimeRoot, 'tmp'),
|
||||||
|
XDG_CACHE_HOME: join(runtimeRoot, 'cache'),
|
||||||
|
XDG_CONFIG_HOME: join(runtimeRoot, 'config'),
|
||||||
|
XDG_STATE_HOME: join(runtimeRoot, 'state'),
|
||||||
|
CI: 'true',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendJavaTmpdirOption(base: string | undefined, tmpDir: string): string {
|
||||||
|
const option = `-Djava.io.tmpdir=${tmpDir}`;
|
||||||
|
if (!base || base.trim().length === 0) return option;
|
||||||
|
if (base.includes(option)) return base;
|
||||||
|
return `${base} ${option}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScriptOutput(stdout: string): Record<string, string> {
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
const lines = stdout.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const normalized = trimmed.startsWith('export ')
|
||||||
|
? trimmed.slice('export '.length).trim()
|
||||||
|
: trimmed;
|
||||||
|
const eq = normalized.indexOf('=');
|
||||||
|
if (eq <= 0) continue;
|
||||||
|
const key = normalized.slice(0, eq).trim();
|
||||||
|
const value = normalized.slice(eq + 1).trim();
|
||||||
|
if (!key) continue;
|
||||||
|
env[key] = value.replace(/^['"]|['"]$/g, '');
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePrepareScript(cwd: string, entry: RuntimePrepareEntry): string {
|
||||||
|
if (entry === 'gradle' || entry === 'node') {
|
||||||
|
return PRESET_SCRIPT_MAP[entry];
|
||||||
|
}
|
||||||
|
return isAbsolute(entry) ? entry : resolve(cwd, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPrepareScript(
|
||||||
|
cwd: string,
|
||||||
|
scriptPath: string,
|
||||||
|
runtimeRoot: string,
|
||||||
|
env: Record<string, string>,
|
||||||
|
): Record<string, string> {
|
||||||
|
if (!existsSync(scriptPath)) {
|
||||||
|
throw new Error(`Runtime prepare script not found: ${scriptPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = spawnSync('bash', [scriptPath], {
|
||||||
|
cwd,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...env,
|
||||||
|
TAKT_RUNTIME_ROOT: runtimeRoot,
|
||||||
|
TAKT_RUNTIME_TMP: join(runtimeRoot, 'tmp'),
|
||||||
|
TAKT_RUNTIME_CACHE: join(runtimeRoot, 'cache'),
|
||||||
|
TAKT_RUNTIME_CONFIG: join(runtimeRoot, 'config'),
|
||||||
|
TAKT_RUNTIME_STATE: join(runtimeRoot, 'state'),
|
||||||
|
},
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const stderr = (result.stderr ?? '').trim();
|
||||||
|
throw new Error(`Runtime prepare script failed: ${scriptPath}${stderr ? ` (${stderr})` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseScriptOutput(result.stdout ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInjectedEnvironment(
|
||||||
|
cwd: string,
|
||||||
|
runtimeRoot: string,
|
||||||
|
prepareEntries: RuntimePrepareEntry[],
|
||||||
|
): Record<string, string> {
|
||||||
|
const env: Record<string, string> = {
|
||||||
|
...createBaseEnvironment(runtimeRoot),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of prepareEntries) {
|
||||||
|
const scriptPath = resolvePrepareScript(cwd, entry);
|
||||||
|
const scriptEnv = runPrepareScript(cwd, scriptPath, runtimeRoot, env);
|
||||||
|
Object.assign(env, scriptEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepareEntries.includes('gradle')) {
|
||||||
|
const tmpDir = env.TMPDIR ?? join(runtimeRoot, 'tmp');
|
||||||
|
env.JAVA_TOOL_OPTIONS = appendJavaTmpdirOption(process.env['JAVA_TOOL_OPTIONS'], tmpDir);
|
||||||
|
}
|
||||||
|
if (prepareEntries.includes('gradle') && !env.GRADLE_USER_HOME) {
|
||||||
|
env.GRADLE_USER_HOME = join(runtimeRoot, 'gradle');
|
||||||
|
}
|
||||||
|
if (prepareEntries.includes('node') && !env.npm_config_cache) {
|
||||||
|
env.npm_config_cache = join(runtimeRoot, 'npm');
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRuntimeDirectories(runtimeRoot: string, env: Record<string, string>): void {
|
||||||
|
const dirs = new Set<string>([
|
||||||
|
runtimeRoot,
|
||||||
|
join(runtimeRoot, 'tmp'),
|
||||||
|
join(runtimeRoot, 'cache'),
|
||||||
|
join(runtimeRoot, 'config'),
|
||||||
|
join(runtimeRoot, 'state'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const value of Object.values(env)) {
|
||||||
|
if (!value || value === 'true') continue;
|
||||||
|
if (value.startsWith(runtimeRoot)) {
|
||||||
|
dirs.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRuntimeEnvFile(envFile: string, env: Record<string, string>): void {
|
||||||
|
const lines = [
|
||||||
|
'#!/usr/bin/env bash',
|
||||||
|
'set -euo pipefail',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
lines.push(`export ${key}=${shellQuote(value)}`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
writeFileSync(envFile, lines.join('\n'), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupePrepare(entries: RuntimePrepareEntry[]): RuntimePrepareEntry[] {
|
||||||
|
return [...new Set(entries)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuntimeConfig(
|
||||||
|
globalRuntime: PieceRuntimeConfig | undefined,
|
||||||
|
pieceRuntime: PieceRuntimeConfig | undefined,
|
||||||
|
): PieceRuntimeConfig | undefined {
|
||||||
|
const prepare = pieceRuntime?.prepare?.length
|
||||||
|
? pieceRuntime.prepare
|
||||||
|
: globalRuntime?.prepare;
|
||||||
|
if (!prepare || prepare.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { prepare: dedupePrepare(prepare) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareRuntimeEnvironment(
|
||||||
|
cwd: string,
|
||||||
|
runtime: PieceRuntimeConfig | undefined,
|
||||||
|
): RuntimeEnvironmentResult | undefined {
|
||||||
|
const prepareEntries = runtime?.prepare;
|
||||||
|
if (!prepareEntries || prepareEntries.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deduped = dedupePrepare(prepareEntries);
|
||||||
|
const runtimeRoot = join(cwd, '.runtime');
|
||||||
|
const envFile = join(runtimeRoot, 'env.sh');
|
||||||
|
const injectedEnv = buildInjectedEnvironment(cwd, runtimeRoot, deduped);
|
||||||
|
|
||||||
|
ensureRuntimeDirectories(runtimeRoot, injectedEnv);
|
||||||
|
writeRuntimeEnvFile(envFile, injectedEnv);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(injectedEnv)) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runtimeRoot,
|
||||||
|
envFile,
|
||||||
|
prepare: deduped,
|
||||||
|
injectedEnv,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -70,6 +70,7 @@ import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
|
|||||||
import { ShutdownManager } from './shutdownManager.js';
|
import { ShutdownManager } from './shutdownManager.js';
|
||||||
import { buildRunPaths } from '../../../core/piece/run/run-paths.js';
|
import { buildRunPaths } from '../../../core/piece/run/run-paths.js';
|
||||||
import { resolveMovementProviderModel } from '../../../core/piece/provider-resolution.js';
|
import { resolveMovementProviderModel } from '../../../core/piece/provider-resolution.js';
|
||||||
|
import { resolveRuntimeConfig } from '../../../core/runtime/runtime-environment.js';
|
||||||
import { writeFileAtomic, ensureDir } from '../../../infra/config/index.js';
|
import { writeFileAtomic, ensureDir } from '../../../infra/config/index.js';
|
||||||
|
|
||||||
const log = createLogger('piece');
|
const log = createLogger('piece');
|
||||||
@ -319,6 +320,10 @@ export async function executePiece(
|
|||||||
const shouldNotifyPieceComplete = shouldNotify && notificationSoundEvents?.pieceComplete !== false;
|
const shouldNotifyPieceComplete = shouldNotify && notificationSoundEvents?.pieceComplete !== false;
|
||||||
const shouldNotifyPieceAbort = shouldNotify && notificationSoundEvents?.pieceAbort !== false;
|
const shouldNotifyPieceAbort = shouldNotify && notificationSoundEvents?.pieceAbort !== false;
|
||||||
const currentProvider = globalConfig.provider ?? 'claude';
|
const currentProvider = globalConfig.provider ?? 'claude';
|
||||||
|
const effectivePieceConfig: PieceConfig = {
|
||||||
|
...pieceConfig,
|
||||||
|
runtime: resolveRuntimeConfig(globalConfig.runtime, pieceConfig.runtime),
|
||||||
|
};
|
||||||
const providerEventLogger = createProviderEventLogger({
|
const providerEventLogger = createProviderEventLogger({
|
||||||
logsDir: runPaths.logsAbs,
|
logsDir: runPaths.logsAbs,
|
||||||
sessionId: pieceSessionId,
|
sessionId: pieceSessionId,
|
||||||
@ -424,7 +429,7 @@ export async function executePiece(
|
|||||||
const runAbortController = new AbortController();
|
const runAbortController = new AbortController();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
engine = new PieceEngine(pieceConfig, cwd, task, {
|
engine = new PieceEngine(effectivePieceConfig, cwd, task, {
|
||||||
abortSignal: runAbortController.signal,
|
abortSignal: runAbortController.signal,
|
||||||
onStream: providerEventLogger.wrapCallback(streamHandler),
|
onStream: providerEventLogger.wrapCallback(streamHandler),
|
||||||
onUserInput,
|
onUserInput,
|
||||||
|
|||||||
@ -126,6 +126,9 @@ export class GlobalConfigManager {
|
|||||||
pieceCategoriesFile: parsed.piece_categories_file,
|
pieceCategoriesFile: parsed.piece_categories_file,
|
||||||
personaProviders: parsed.persona_providers,
|
personaProviders: parsed.persona_providers,
|
||||||
providerOptions: normalizeProviderOptions(parsed.provider_options),
|
providerOptions: normalizeProviderOptions(parsed.provider_options),
|
||||||
|
runtime: parsed.runtime?.prepare && parsed.runtime.prepare.length > 0
|
||||||
|
? { prepare: [...new Set(parsed.runtime.prepare)] }
|
||||||
|
: undefined,
|
||||||
branchNameStrategy: parsed.branch_name_strategy,
|
branchNameStrategy: parsed.branch_name_strategy,
|
||||||
preventSleep: parsed.prevent_sleep,
|
preventSleep: parsed.prevent_sleep,
|
||||||
notificationSound: parsed.notification_sound,
|
notificationSound: parsed.notification_sound,
|
||||||
@ -210,6 +213,11 @@ export class GlobalConfigManager {
|
|||||||
if (config.personaProviders && Object.keys(config.personaProviders).length > 0) {
|
if (config.personaProviders && Object.keys(config.personaProviders).length > 0) {
|
||||||
raw.persona_providers = config.personaProviders;
|
raw.persona_providers = config.personaProviders;
|
||||||
}
|
}
|
||||||
|
if (config.runtime?.prepare && config.runtime.prepare.length > 0) {
|
||||||
|
raw.runtime = {
|
||||||
|
prepare: [...new Set(config.runtime.prepare)],
|
||||||
|
};
|
||||||
|
}
|
||||||
if (config.branchNameStrategy) {
|
if (config.branchNameStrategy) {
|
||||||
raw.branch_name_strategy = config.branchNameStrategy;
|
raw.branch_name_strategy = config.branchNameStrategy;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,10 @@ import {
|
|||||||
} from './resource-resolver.js';
|
} from './resource-resolver.js';
|
||||||
|
|
||||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
||||||
|
type RawPiece = z.output<typeof PieceConfigRawSchema>;
|
||||||
|
|
||||||
import type { MovementProviderOptions } from '../../../core/models/piece-types.js';
|
import type { MovementProviderOptions } from '../../../core/models/piece-types.js';
|
||||||
|
import type { PieceRuntimeConfig } from '../../../core/models/piece-types.js';
|
||||||
|
|
||||||
/** Convert raw YAML provider_options (snake_case) to internal format (camelCase). */
|
/** Convert raw YAML provider_options (snake_case) to internal format (camelCase). */
|
||||||
export function normalizeProviderOptions(
|
export function normalizeProviderOptions(
|
||||||
@ -81,6 +83,16 @@ export function mergeProviderOptions(
|
|||||||
return Object.keys(result).length > 0 ? result : undefined;
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRuntimeConfig(raw: RawPiece['piece_config']): PieceRuntimeConfig | undefined {
|
||||||
|
const prepare = raw?.runtime?.prepare;
|
||||||
|
if (!prepare || prepare.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
prepare: [...new Set(prepare)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Check if a raw output contract item is the object form (has 'name' property). */
|
/** Check if a raw output contract item is the object form (has 'name' property). */
|
||||||
function isOutputContractItem(raw: unknown): raw is { name: string; order?: string; format?: string } {
|
function isOutputContractItem(raw: unknown): raw is { name: string; order?: string; format?: string } {
|
||||||
return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw;
|
return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw;
|
||||||
@ -389,6 +401,7 @@ export function normalizePieceConfig(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pieceProviderOptions = normalizeProviderOptions(parsed.piece_config?.provider_options as RawStep['provider_options']);
|
const pieceProviderOptions = normalizeProviderOptions(parsed.piece_config?.provider_options as RawStep['provider_options']);
|
||||||
|
const pieceRuntime = normalizeRuntimeConfig(parsed.piece_config);
|
||||||
|
|
||||||
const movements: PieceMovement[] = parsed.movements.map((step) =>
|
const movements: PieceMovement[] = parsed.movements.map((step) =>
|
||||||
normalizeStepFromRaw(step, pieceDir, sections, pieceProviderOptions, context),
|
normalizeStepFromRaw(step, pieceDir, sections, pieceProviderOptions, context),
|
||||||
@ -401,6 +414,7 @@ export function normalizePieceConfig(
|
|||||||
name: parsed.name,
|
name: parsed.name,
|
||||||
description: parsed.description,
|
description: parsed.description,
|
||||||
providerOptions: pieceProviderOptions,
|
providerOptions: pieceProviderOptions,
|
||||||
|
runtime: pieceRuntime,
|
||||||
personas: parsed.personas,
|
personas: parsed.personas,
|
||||||
policies: resolvedPolicies,
|
policies: resolvedPolicies,
|
||||||
knowledge: resolvedKnowledge,
|
knowledge: resolvedKnowledge,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user