fix(harness): allow explicit Codex tool directories

This commit is contained in:
KOKO\Mimi
2026-07-29 23:57:46 +09:00
parent b0ab8e77d5
commit 026c9eea7c
3 changed files with 251 additions and 18 deletions
+115 -18
View File
@@ -3,13 +3,14 @@
Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
Usage:
python scripts/execute.py <phase-dir> [--push]
python scripts/execute.py <phase-dir> [--push] [--codex-add-dir DIR]
"""
import argparse
import contextlib
import json
import os
import shutil
import subprocess
import sys
import threading
@@ -26,6 +27,78 @@ class CodexEnvironmentError(RuntimeError):
"""재시도로 해결할 수 없는 Codex CLI 환경 오류."""
def normalize_codex_add_dirs(raw_dirs) -> tuple[Path, ...]:
"""검증된 Codex 추가 writable directory를 반환한다."""
normalized = []
for raw in raw_dirs:
candidate = Path(raw)
if not candidate.is_absolute():
raise CodexEnvironmentError(
f"Codex 추가 경로는 절대 경로여야 합니다: {raw}"
)
resolved = candidate.resolve()
if not resolved.is_dir():
raise CodexEnvironmentError(
f"Codex 추가 경로가 존재하는 디렉터리가 아닙니다: {resolved}"
)
normalized.append(resolved)
return tuple(normalized)
def build_codex_command(
codex_executable: Path,
root: Path,
add_dirs: tuple[Path, ...],
) -> list[str]:
"""workspace-write Codex command를 안전한 argv로 구성한다."""
command = [
str(codex_executable.resolve()),
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
]
for path in add_dirs:
command.extend(("--add-dir", str(path)))
command.extend(("--cd", str(root.resolve()), "-"))
return command
def build_codex_environment(
base_env,
add_dirs: tuple[Path, ...],
) -> dict[str, str]:
"""명시적 tool directory를 기존 PATH 뒤에 추가한다."""
environment = dict(base_env)
path_entries = [environment.get("PATH", "")]
path_entries.extend(str(path) for path in add_dirs)
environment["PATH"] = os.pathsep.join(entry for entry in path_entries if entry)
return environment
def run_utf8_process(command, *, cwd, prompt, env, timeout):
"""UTF-8 prompt/output 계약으로 subprocess를 실행한다."""
return subprocess.run(
list(command),
cwd=cwd,
input=prompt,
capture_output=True,
encoding="utf-8",
errors="replace",
env=env,
timeout=timeout,
)
def configure_standard_streams(*streams) -> None:
"""Windows legacy console에서 status 출력이 중단되지 않게 한다."""
for stream in streams:
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is not None:
reconfigure(errors="replace")
@contextlib.contextmanager
def progress_indicator(label: str):
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
@@ -62,13 +135,20 @@ class StepExecutor:
CHORE_MSG = "chore({phase}): step {num} output"
TZ = timezone(timedelta(hours=9))
def __init__(self, phase_dir_name: str, *, auto_push: bool = False):
def __init__(
self,
phase_dir_name: str,
*,
auto_push: bool = False,
codex_add_dirs=(),
):
self._root = str(ROOT)
self._phases_dir = ROOT / "phases"
self._phase_dir = self._phases_dir / phase_dir_name
self._phase_dir_name = phase_dir_name
self._top_index_file = self._phases_dir / "index.json"
self._auto_push = auto_push
self._codex_add_dirs = normalize_codex_add_dirs(codex_add_dirs)
if not self._phase_dir.is_dir():
print(f"ERROR: {self._phase_dir} not found")
@@ -241,24 +321,26 @@ class StepExecutor:
sys.exit(1)
prompt = preamble + step_file.read_text(encoding="utf-8")
command = [
"codex",
"exec",
"--json",
"--sandbox",
"workspace-write",
"--dangerously-bypass-hook-trust",
"--cd",
self._root,
"-",
]
codex_executable = shutil.which("codex")
if codex_executable is None:
raise CodexEnvironmentError(
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
)
command = build_codex_command(
Path(codex_executable),
Path(self._root),
self._codex_add_dirs,
)
environment = build_codex_environment(
os.environ,
self._codex_add_dirs,
)
try:
result = subprocess.run(
result = run_utf8_process(
command,
cwd=self._root,
input=prompt,
capture_output=True,
text=True,
prompt=prompt,
env=environment,
timeout=1800,
)
except FileNotFoundError as exc:
@@ -465,13 +547,28 @@ class StepExecutor:
def main():
configure_standard_streams(sys.stdout, sys.stderr)
parser = argparse.ArgumentParser(description="Harness Step Executor")
parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)")
parser.add_argument("--push", action="store_true", help="Push branch after completion")
parser.add_argument(
"--codex-add-dir",
action="append",
default=[],
metavar="DIR",
help=(
"Codex workspace-write sandbox에 추가할 절대 tool/runtime directory. "
"여러 번 지정할 수 있습니다."
),
)
args = parser.parse_args()
try:
StepExecutor(args.phase_dir, auto_push=args.push).run()
StepExecutor(
args.phase_dir,
auto_push=args.push,
codex_add_dirs=args.codex_add_dir,
).run()
except CodexEnvironmentError as exc:
print(f"ERROR: {exc}")
sys.exit(1)