nasde-toolkit 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nasde_toolkit/__init__.py +5 -0
- nasde_toolkit/_bundled_skills/nasde-benchmark-creator/SKILL.md +321 -0
- nasde_toolkit/_bundled_skills/nasde-benchmark-from-history/SKILL.md +298 -0
- nasde_toolkit/_bundled_skills/nasde-benchmark-from-public-repos/SKILL.md +303 -0
- nasde_toolkit/_bundled_skills/nasde-benchmark-runner/SKILL.md +388 -0
- nasde_toolkit/_version.py +24 -0
- nasde_toolkit/agents/__init__.py +1 -0
- nasde_toolkit/agents/configurable_claude.py +121 -0
- nasde_toolkit/agents/configurable_codex.py +101 -0
- nasde_toolkit/agents/configurable_gemini.py +194 -0
- nasde_toolkit/banner.py +82 -0
- nasde_toolkit/cli.py +513 -0
- nasde_toolkit/config.py +206 -0
- nasde_toolkit/docker.py +221 -0
- nasde_toolkit/evaluator.py +663 -0
- nasde_toolkit/evaluator_backends/__init__.py +21 -0
- nasde_toolkit/evaluator_backends/claude_subprocess.py +172 -0
- nasde_toolkit/evaluator_backends/codex_subprocess.py +152 -0
- nasde_toolkit/evaluator_backends/protocol.py +30 -0
- nasde_toolkit/runner.py +739 -0
- nasde_toolkit/scaffold/__init__.py +161 -0
- nasde_toolkit/skills_installer.py +83 -0
- nasde_toolkit/update_check.py +166 -0
- nasde_toolkit-0.3.2.dist-info/METADATA +701 -0
- nasde_toolkit-0.3.2.dist-info/RECORD +28 -0
- nasde_toolkit-0.3.2.dist-info/WHEEL +4 -0
- nasde_toolkit-0.3.2.dist-info/entry_points.txt +2 -0
- nasde_toolkit-0.3.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Claude Code CLI subprocess evaluator backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from nasde_toolkit.config import EvaluationConfig
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ClaudeSubprocessBackend:
|
|
20
|
+
"""Evaluator backend that spawns `claude -p` as a subprocess.
|
|
21
|
+
|
|
22
|
+
Uses `--output-format json` to get structured output with a `result`
|
|
23
|
+
field containing the agent's text response. Authenticates via whatever
|
|
24
|
+
credentials are configured (ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN).
|
|
25
|
+
|
|
26
|
+
Does NOT use `--bare` mode — preserves OAuth/keychain auth reads
|
|
27
|
+
(required for subscription-based billing) and auto-discovery of user-level
|
|
28
|
+
skills. Trades a slightly slower startup for full compatibility with
|
|
29
|
+
subscription accounts.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
async def run_evaluation(
|
|
33
|
+
self,
|
|
34
|
+
prompt: str,
|
|
35
|
+
workspace_path: Path,
|
|
36
|
+
eval_config: EvaluationConfig,
|
|
37
|
+
project_root: Path,
|
|
38
|
+
trial_dir: Path | None = None,
|
|
39
|
+
) -> str:
|
|
40
|
+
self.validate_cli_installed()
|
|
41
|
+
self.validate_auth()
|
|
42
|
+
cmd, temp_dir = self._build_command_with_skills(workspace_path, eval_config, project_root, trial_dir)
|
|
43
|
+
env = self._build_env()
|
|
44
|
+
cwd = temp_dir if temp_dir else workspace_path
|
|
45
|
+
try:
|
|
46
|
+
return await self._run_subprocess(cmd, prompt, cwd, env)
|
|
47
|
+
finally:
|
|
48
|
+
if temp_dir:
|
|
49
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
50
|
+
|
|
51
|
+
def validate_cli_installed(self) -> None:
|
|
52
|
+
if shutil.which("claude") is not None:
|
|
53
|
+
return
|
|
54
|
+
console.print(
|
|
55
|
+
"[red]ERROR: `claude` CLI not found on PATH.[/red]\n"
|
|
56
|
+
"[yellow]Assessment evaluation with the Claude backend requires the Claude Code CLI.[/yellow]\n"
|
|
57
|
+
"Install it from https://docs.claude.com/en/docs/claude-code/setup, "
|
|
58
|
+
'then re-run. To use a different backend, set \\[evaluation] backend = "codex" '
|
|
59
|
+
"in nasde.toml, or pass --without-eval to skip assessment evaluation."
|
|
60
|
+
)
|
|
61
|
+
raise SystemExit(1)
|
|
62
|
+
|
|
63
|
+
def validate_auth(self) -> None:
|
|
64
|
+
has_api_key = bool(os.environ.get("ANTHROPIC_API_KEY"))
|
|
65
|
+
has_oauth = bool(os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"))
|
|
66
|
+
if not has_api_key and not has_oauth:
|
|
67
|
+
console.print("[red]ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN[/red]")
|
|
68
|
+
raise SystemExit(1)
|
|
69
|
+
|
|
70
|
+
def _build_command_with_skills(
|
|
71
|
+
self,
|
|
72
|
+
workspace_path: Path,
|
|
73
|
+
eval_config: EvaluationConfig,
|
|
74
|
+
project_root: Path,
|
|
75
|
+
trial_dir: Path | None,
|
|
76
|
+
) -> tuple[list[str], Path | None]:
|
|
77
|
+
cmd = self._build_command(workspace_path, eval_config, project_root, trial_dir)
|
|
78
|
+
temp_dir: Path | None = None
|
|
79
|
+
|
|
80
|
+
if eval_config.skills_dir:
|
|
81
|
+
skills_source = _resolve_path(eval_config.skills_dir, project_root)
|
|
82
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="nasde_eval_"))
|
|
83
|
+
target = temp_dir / ".claude" / "skills"
|
|
84
|
+
if skills_source.is_dir():
|
|
85
|
+
shutil.copytree(skills_source, target)
|
|
86
|
+
cmd.extend(["--add-dir", str(workspace_path)])
|
|
87
|
+
|
|
88
|
+
return cmd, temp_dir
|
|
89
|
+
|
|
90
|
+
def _build_command(
|
|
91
|
+
self,
|
|
92
|
+
workspace_path: Path,
|
|
93
|
+
eval_config: EvaluationConfig,
|
|
94
|
+
project_root: Path,
|
|
95
|
+
trial_dir: Path | None,
|
|
96
|
+
) -> list[str]:
|
|
97
|
+
cmd = [
|
|
98
|
+
"claude",
|
|
99
|
+
"-p",
|
|
100
|
+
"--output-format",
|
|
101
|
+
"json",
|
|
102
|
+
"--no-session-persistence",
|
|
103
|
+
"--model",
|
|
104
|
+
eval_config.model,
|
|
105
|
+
"--max-turns",
|
|
106
|
+
str(eval_config.max_turns),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
allowed_tools = eval_config.allowed_tools or ["Read", "Glob", "Grep"]
|
|
110
|
+
cmd.extend(["--allowedTools", ",".join(allowed_tools)])
|
|
111
|
+
|
|
112
|
+
if eval_config.include_trajectory and trial_dir:
|
|
113
|
+
cmd.extend(["--add-dir", str(trial_dir)])
|
|
114
|
+
|
|
115
|
+
if eval_config.mcp_config:
|
|
116
|
+
mcp_path = _resolve_path(eval_config.mcp_config, project_root)
|
|
117
|
+
cmd.extend(["--mcp-config", str(mcp_path)])
|
|
118
|
+
|
|
119
|
+
if eval_config.append_system_prompt:
|
|
120
|
+
cmd.extend(["--append-system-prompt", eval_config.append_system_prompt])
|
|
121
|
+
|
|
122
|
+
return cmd
|
|
123
|
+
|
|
124
|
+
def _build_env(self) -> dict[str, str]:
|
|
125
|
+
env = dict(os.environ)
|
|
126
|
+
env.pop("CLAUDECODE", None)
|
|
127
|
+
return env
|
|
128
|
+
|
|
129
|
+
async def _run_subprocess(
|
|
130
|
+
self,
|
|
131
|
+
cmd: list[str],
|
|
132
|
+
prompt: str,
|
|
133
|
+
workspace_path: Path,
|
|
134
|
+
env: dict[str, str],
|
|
135
|
+
) -> str:
|
|
136
|
+
process = await asyncio.create_subprocess_exec(
|
|
137
|
+
*cmd,
|
|
138
|
+
stdin=asyncio.subprocess.PIPE,
|
|
139
|
+
stdout=asyncio.subprocess.PIPE,
|
|
140
|
+
stderr=asyncio.subprocess.PIPE,
|
|
141
|
+
cwd=str(workspace_path),
|
|
142
|
+
env=env,
|
|
143
|
+
)
|
|
144
|
+
stdout_bytes, stderr_bytes = await process.communicate(input=prompt.encode())
|
|
145
|
+
|
|
146
|
+
if process.returncode != 0:
|
|
147
|
+
stderr_text = stderr_bytes.decode(errors="replace").strip()
|
|
148
|
+
last_lines = "\n".join(stderr_text.splitlines()[-10:]) if stderr_text else ""
|
|
149
|
+
model = cmd[cmd.index("--model") + 1]
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
f"Claude Code process failed (exit code {process.returncode}). "
|
|
152
|
+
f"Model: {model}, cwd: {workspace_path}. "
|
|
153
|
+
f"stderr (last 10 lines):\n{last_lines}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return _extract_result_text(stdout_bytes.decode())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _resolve_path(relative_or_absolute: str, project_root: Path) -> Path:
|
|
160
|
+
path = Path(relative_or_absolute)
|
|
161
|
+
if path.is_absolute():
|
|
162
|
+
return path
|
|
163
|
+
return project_root / path
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _extract_result_text(stdout: str) -> str:
|
|
167
|
+
try:
|
|
168
|
+
data = json.loads(stdout)
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
return stdout
|
|
171
|
+
result = data.get("result", "")
|
|
172
|
+
return result if isinstance(result, str) else ""
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Codex CLI subprocess evaluator backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from nasde_toolkit.config import EvaluationConfig
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CodexSubprocessBackend:
|
|
19
|
+
"""Evaluator backend that spawns `codex exec` as a subprocess.
|
|
20
|
+
|
|
21
|
+
Uses `--json` for JSONL event streaming on stdout. The CLI writes clean
|
|
22
|
+
NDJSON to stdout (banner and diagnostics go to stderr), so parsing is
|
|
23
|
+
straightforward. Extracts the final `agent_message` items as the
|
|
24
|
+
evaluation response. Authenticates via OPENAI_API_KEY, CODEX_API_KEY, or
|
|
25
|
+
ChatGPT OAuth (`~/.codex/auth.json` created by `codex login`).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
async def run_evaluation(
|
|
29
|
+
self,
|
|
30
|
+
prompt: str,
|
|
31
|
+
workspace_path: Path,
|
|
32
|
+
eval_config: EvaluationConfig,
|
|
33
|
+
project_root: Path,
|
|
34
|
+
trial_dir: Path | None = None,
|
|
35
|
+
) -> str:
|
|
36
|
+
self.validate_cli_installed()
|
|
37
|
+
self.validate_auth()
|
|
38
|
+
cmd = self._build_command(workspace_path, eval_config)
|
|
39
|
+
cmd.append("-")
|
|
40
|
+
env = self._build_env()
|
|
41
|
+
return await self._run_subprocess(cmd, prompt, workspace_path, env)
|
|
42
|
+
|
|
43
|
+
def _build_env(self) -> dict[str, str]:
|
|
44
|
+
env = dict(os.environ)
|
|
45
|
+
has_openai_key = bool(env.get("OPENAI_API_KEY"))
|
|
46
|
+
has_codex_key = bool(env.get("CODEX_API_KEY"))
|
|
47
|
+
if not has_openai_key and not has_codex_key:
|
|
48
|
+
return env
|
|
49
|
+
if _has_chatgpt_oauth() and not has_openai_key:
|
|
50
|
+
env.pop("CODEX_API_KEY", None)
|
|
51
|
+
return env
|
|
52
|
+
if has_codex_key and not has_openai_key:
|
|
53
|
+
env["OPENAI_API_KEY"] = env["CODEX_API_KEY"]
|
|
54
|
+
return env
|
|
55
|
+
|
|
56
|
+
def validate_cli_installed(self) -> None:
|
|
57
|
+
if shutil.which("codex") is not None:
|
|
58
|
+
return
|
|
59
|
+
console.print(
|
|
60
|
+
"[red]ERROR: `codex` CLI not found on PATH.[/red]\n"
|
|
61
|
+
"[yellow]Assessment evaluation with the Codex backend requires the Codex CLI.[/yellow]\n"
|
|
62
|
+
"Install it from https://github.com/openai/codex, then re-run. "
|
|
63
|
+
'To use a different backend, set \\[evaluation] backend = "claude" in nasde.toml, '
|
|
64
|
+
"or pass --without-eval to skip assessment evaluation."
|
|
65
|
+
)
|
|
66
|
+
raise SystemExit(1)
|
|
67
|
+
|
|
68
|
+
def validate_auth(self) -> None:
|
|
69
|
+
has_openai_key = bool(os.environ.get("OPENAI_API_KEY"))
|
|
70
|
+
has_codex_key = bool(os.environ.get("CODEX_API_KEY"))
|
|
71
|
+
has_chatgpt_oauth = _has_chatgpt_oauth()
|
|
72
|
+
if not has_openai_key and not has_codex_key and not has_chatgpt_oauth:
|
|
73
|
+
console.print("[red]ERROR: Set OPENAI_API_KEY, CODEX_API_KEY, or log in via `codex login`[/red]")
|
|
74
|
+
raise SystemExit(1)
|
|
75
|
+
|
|
76
|
+
def _build_command(
|
|
77
|
+
self,
|
|
78
|
+
workspace_path: Path,
|
|
79
|
+
eval_config: EvaluationConfig,
|
|
80
|
+
) -> list[str]:
|
|
81
|
+
cmd = [
|
|
82
|
+
"codex",
|
|
83
|
+
"exec",
|
|
84
|
+
"--json",
|
|
85
|
+
"--full-auto",
|
|
86
|
+
"--skip-git-repo-check",
|
|
87
|
+
"--color",
|
|
88
|
+
"never",
|
|
89
|
+
"--model",
|
|
90
|
+
eval_config.model,
|
|
91
|
+
]
|
|
92
|
+
return cmd
|
|
93
|
+
|
|
94
|
+
async def _run_subprocess(
|
|
95
|
+
self,
|
|
96
|
+
cmd: list[str],
|
|
97
|
+
prompt: str,
|
|
98
|
+
workspace_path: Path,
|
|
99
|
+
env: dict[str, str],
|
|
100
|
+
) -> str:
|
|
101
|
+
process = await asyncio.create_subprocess_exec(
|
|
102
|
+
*cmd,
|
|
103
|
+
stdin=asyncio.subprocess.PIPE,
|
|
104
|
+
stdout=asyncio.subprocess.PIPE,
|
|
105
|
+
stderr=asyncio.subprocess.PIPE,
|
|
106
|
+
cwd=str(workspace_path),
|
|
107
|
+
env=env,
|
|
108
|
+
)
|
|
109
|
+
stdout_bytes, stderr_bytes = await process.communicate(input=prompt.encode())
|
|
110
|
+
|
|
111
|
+
stdout_text = stdout_bytes.decode(errors="replace")
|
|
112
|
+
if process.returncode != 0:
|
|
113
|
+
stderr_text = stderr_bytes.decode(errors="replace").strip()
|
|
114
|
+
stderr_tail = "\n".join(stderr_text.splitlines()[-10:]) or "<empty>"
|
|
115
|
+
stdout_tail = "\n".join(stdout_text.strip().splitlines()[-10:]) or "<empty>"
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"Codex process failed (exit code {process.returncode}). "
|
|
118
|
+
f"cwd: {workspace_path}. "
|
|
119
|
+
f"stderr (last 10 lines):\n{stderr_tail}\n"
|
|
120
|
+
f"stdout (last 10 lines):\n{stdout_tail}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return _extract_agent_messages(stdout_text)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _has_chatgpt_oauth() -> bool:
|
|
127
|
+
auth_path = Path.home() / ".codex" / "auth.json"
|
|
128
|
+
if not auth_path.exists():
|
|
129
|
+
return False
|
|
130
|
+
try:
|
|
131
|
+
raw = json.loads(auth_path.read_text())
|
|
132
|
+
except (json.JSONDecodeError, OSError):
|
|
133
|
+
return False
|
|
134
|
+
return raw.get("auth_mode") == "chatgpt" and bool(raw.get("tokens", {}).get("access_token"))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _extract_agent_messages(stdout: str) -> str:
|
|
138
|
+
messages: list[str] = []
|
|
139
|
+
for line in stdout.strip().splitlines():
|
|
140
|
+
if not line.strip():
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
event = json.loads(line)
|
|
144
|
+
except json.JSONDecodeError:
|
|
145
|
+
continue
|
|
146
|
+
if event.get("type") == "item.completed":
|
|
147
|
+
item = event.get("item", {})
|
|
148
|
+
if item.get("type") == "agent_message":
|
|
149
|
+
text = item.get("text", "")
|
|
150
|
+
if text:
|
|
151
|
+
messages.append(text)
|
|
152
|
+
return "\n".join(messages)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Evaluator backend protocol — defines the interface for CLI-based evaluators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from nasde_toolkit.config import EvaluationConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class EvaluatorBackend(Protocol):
|
|
13
|
+
"""Interface for subprocess-based evaluator backends.
|
|
14
|
+
|
|
15
|
+
Each backend spawns a CLI agent (claude, codex, etc.) as a subprocess,
|
|
16
|
+
passes the evaluation prompt, and returns the agent's text response.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
async def run_evaluation(
|
|
20
|
+
self,
|
|
21
|
+
prompt: str,
|
|
22
|
+
workspace_path: Path,
|
|
23
|
+
eval_config: EvaluationConfig,
|
|
24
|
+
project_root: Path,
|
|
25
|
+
trial_dir: Path | None = None,
|
|
26
|
+
) -> str: ...
|
|
27
|
+
|
|
28
|
+
def validate_cli_installed(self) -> None: ...
|
|
29
|
+
|
|
30
|
+
def validate_auth(self) -> None: ...
|