@andespindola/crow-skills 0.40.0 → 0.41.0
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.
- package/crow_skills/__init__.py +1 -1
- package/package.json +1 -1
- package/release.json +33 -8
- package/skills/crow-mcp-maker/SKILL.md +6 -6
- package/skills/crow-mcp-maker/references/full-runtime.md +88 -0
- package/skills/crow-mcp-maker/references/security-and-cost.md +1 -1
- package/skills/crow-mcp-maker/scripts/engines.py +732 -0
- package/skills/crow-skill-maker/SKILL.md +6 -5
- package/skills/crow-skill-maker/references/full-runtime.md +108 -0
- package/skills/crow-skill-maker/references/security-and-cost.md +2 -2
- package/skills/crow-skill-maker/scripts/creator.py +839 -0
- package/skills/crow-skill-maker/scripts/engines.py +732 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explicit isolated native-Claude Skill Creator runtime; never a Codex equivalence claim."""
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import hashlib
|
|
6
|
+
import fcntl
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path, PurePosixPath
|
|
11
|
+
import re
|
|
12
|
+
import runpy
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
import selectors
|
|
15
|
+
import shutil
|
|
16
|
+
import signal
|
|
17
|
+
import stat
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
import time
|
|
22
|
+
import threading
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
SCHEMA = 'crow-maker-creator/1'
|
|
26
|
+
MANIFEST_SHA256 = '002bd425fe66fba00460e4b853cf2ce9955877ca309528f98cac00973195a023'
|
|
27
|
+
COMMIT = '2c5958ecac7cee2b583967a2c4981d0f4a75c579'
|
|
28
|
+
DEPENDENCIES = ['PyYAML==6.0.3']
|
|
29
|
+
SCRIPTS = {name: 'scripts/' + name + '.py' for name in (
|
|
30
|
+
'run_eval', 'run_loop', 'improve_description', 'quick_validate',
|
|
31
|
+
'package_skill', 'aggregate_benchmark', 'generate_report')}
|
|
32
|
+
SCRIPTS['generate_review'] = 'eval-viewer/generate_review.py'
|
|
33
|
+
VENDOR = Path(__file__).absolute().parent.parent / 'vendor'
|
|
34
|
+
|
|
35
|
+
class CreatorError(ValueError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def _hash(raw):
|
|
39
|
+
return hashlib.sha256(raw).hexdigest()
|
|
40
|
+
|
|
41
|
+
def _path(path, missing=False):
|
|
42
|
+
p = Path(os.path.abspath(os.fspath(path)))
|
|
43
|
+
for part in [*reversed(p.parents), p]:
|
|
44
|
+
try:
|
|
45
|
+
info = part.lstat()
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
if missing:
|
|
48
|
+
continue
|
|
49
|
+
raise CreatorError('path-unavailable') from None
|
|
50
|
+
if stat.S_ISLNK(info.st_mode):
|
|
51
|
+
raise CreatorError('symlink-path-rejected')
|
|
52
|
+
return p
|
|
53
|
+
|
|
54
|
+
def _read(path, limit=16*1024*1024):
|
|
55
|
+
p = _path(path)
|
|
56
|
+
fd = os.open(p.anchor, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
57
|
+
try:
|
|
58
|
+
for i, part in enumerate(p.parts[1:]):
|
|
59
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW | getattr(os, 'O_NONBLOCK', 0)
|
|
60
|
+
if i < len(p.parts[1:])-1:
|
|
61
|
+
flags |= os.O_DIRECTORY
|
|
62
|
+
child = os.open(part, flags, dir_fd=fd)
|
|
63
|
+
os.close(fd)
|
|
64
|
+
fd = child
|
|
65
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
66
|
+
raise CreatorError('nonregular-file')
|
|
67
|
+
chunks, remaining = [], limit+1
|
|
68
|
+
while remaining:
|
|
69
|
+
chunk = os.read(fd, min(65536, remaining))
|
|
70
|
+
if not chunk:
|
|
71
|
+
break
|
|
72
|
+
chunks.append(chunk)
|
|
73
|
+
remaining -= len(chunk)
|
|
74
|
+
raw = b''.join(chunks)
|
|
75
|
+
if len(raw) > limit:
|
|
76
|
+
raise CreatorError('file-too-large')
|
|
77
|
+
return raw
|
|
78
|
+
finally:
|
|
79
|
+
os.close(fd)
|
|
80
|
+
|
|
81
|
+
def _private_directory(path, create=False):
|
|
82
|
+
p = _path(path, missing=create)
|
|
83
|
+
if create:
|
|
84
|
+
# Only caller-provided final directory: parents must already be safe.
|
|
85
|
+
_path(p.parent)
|
|
86
|
+
p.mkdir(mode=0o700, exist_ok=True)
|
|
87
|
+
info = p.stat()
|
|
88
|
+
if not p.is_dir() or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077:
|
|
89
|
+
raise CreatorError('directory-must-be-owned-and-private')
|
|
90
|
+
return p
|
|
91
|
+
|
|
92
|
+
def _write(path, raw, mode=0o600):
|
|
93
|
+
p = _path(path, missing=True)
|
|
94
|
+
fd = os.open(p.anchor,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
|
|
95
|
+
try:
|
|
96
|
+
for part in p.parts[1:-1]:
|
|
97
|
+
child=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)
|
|
98
|
+
os.close(fd);fd=child
|
|
99
|
+
out=os.open(p.name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,mode,dir_fd=fd)
|
|
100
|
+
with os.fdopen(out,'wb') as stream:
|
|
101
|
+
stream.write(raw)
|
|
102
|
+
finally:
|
|
103
|
+
os.close(fd)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _donor_files():
|
|
107
|
+
raw = _read(VENDOR / 'vendor-manifest.json')
|
|
108
|
+
if _hash(raw) != MANIFEST_SHA256:
|
|
109
|
+
raise CreatorError('manifest-hash-mismatch')
|
|
110
|
+
manifest = json.loads(raw)
|
|
111
|
+
prefix = 'skills/crow-skill-maker/vendor/skill-creator/'
|
|
112
|
+
files = {}
|
|
113
|
+
for entry in manifest['files']:
|
|
114
|
+
copied = entry['copiedAs']
|
|
115
|
+
if not copied.startswith(prefix):
|
|
116
|
+
continue
|
|
117
|
+
relative = copied[len(prefix):]
|
|
118
|
+
parts = PurePosixPath(relative).parts
|
|
119
|
+
if not parts or '..' in parts or PurePosixPath(relative).is_absolute() or '\\' in relative:
|
|
120
|
+
raise CreatorError('manifest-path-escape')
|
|
121
|
+
if entry['repository'] != 'anthropics/skills' or entry['commit'] != COMMIT or relative in files:
|
|
122
|
+
raise CreatorError('unexpected-source-identity')
|
|
123
|
+
raw = _read(VENDOR / 'skill-creator' / relative)
|
|
124
|
+
if _hash(raw) != entry['upstreamSha256']:
|
|
125
|
+
raise CreatorError('donor-hash-mismatch')
|
|
126
|
+
files[relative] = raw
|
|
127
|
+
expected = set(files)
|
|
128
|
+
actual = set()
|
|
129
|
+
for directory, dirs, names in os.walk(_path(VENDOR / 'skill-creator'), followlinks=False):
|
|
130
|
+
for name in dirs + names:
|
|
131
|
+
path = Path(directory) / name
|
|
132
|
+
if path.is_symlink():
|
|
133
|
+
raise CreatorError('donor-symlink')
|
|
134
|
+
for name in names:
|
|
135
|
+
actual.add((Path(directory)/name).relative_to(VENDOR/'skill-creator').as_posix())
|
|
136
|
+
if actual != expected or not all(v in files for v in SCRIPTS.values()) or 'SKILL.md.source' not in files:
|
|
137
|
+
raise CreatorError('donor-file-set-mismatch')
|
|
138
|
+
return files
|
|
139
|
+
|
|
140
|
+
def _kill_group_impl(process):
|
|
141
|
+
try:
|
|
142
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
143
|
+
except ProcessLookupError:
|
|
144
|
+
pass
|
|
145
|
+
try:
|
|
146
|
+
process.wait(timeout=.5)
|
|
147
|
+
except subprocess.TimeoutExpired:
|
|
148
|
+
pass
|
|
149
|
+
# Leader may have exited while descendants remain; still signal its group.
|
|
150
|
+
try:
|
|
151
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
152
|
+
except ProcessLookupError:
|
|
153
|
+
pass
|
|
154
|
+
try:
|
|
155
|
+
process.wait(timeout=2)
|
|
156
|
+
except subprocess.TimeoutExpired:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
class _Cancellation(KeyboardInterrupt):
|
|
160
|
+
def __init__(self,signum):
|
|
161
|
+
self.signum=signum
|
|
162
|
+
|
|
163
|
+
class _SignalGuard:
|
|
164
|
+
def __init__(self):
|
|
165
|
+
self.depth=0;self.pending=None
|
|
166
|
+
def handler(self,signum,frame):
|
|
167
|
+
if self.depth:
|
|
168
|
+
if self.pending is None:self.pending=signum
|
|
169
|
+
else:raise _Cancellation(signum)
|
|
170
|
+
@contextlib.contextmanager
|
|
171
|
+
def deferred(self):
|
|
172
|
+
self.depth+=1
|
|
173
|
+
try:yield
|
|
174
|
+
finally:
|
|
175
|
+
self.depth-=1
|
|
176
|
+
if not self.depth and self.pending is not None:
|
|
177
|
+
signum,self.pending=self.pending,None
|
|
178
|
+
raise _Cancellation(signum)
|
|
179
|
+
|
|
180
|
+
@contextlib.contextmanager
|
|
181
|
+
def _cancellation_scope():
|
|
182
|
+
guard=_SignalGuard()
|
|
183
|
+
installed=threading.current_thread() is threading.main_thread()
|
|
184
|
+
previous={sig:signal.getsignal(sig) for sig in (signal.SIGTERM,signal.SIGINT)} if installed else {}
|
|
185
|
+
if installed:
|
|
186
|
+
for sig in previous:signal.signal(sig,guard.handler)
|
|
187
|
+
try:yield guard
|
|
188
|
+
finally:
|
|
189
|
+
for sig,handler in previous.items():signal.signal(sig,handler)
|
|
190
|
+
|
|
191
|
+
def _kill_group(process):
|
|
192
|
+
# Parent-only handler deferral cannot leak a blocked mask into exec. A
|
|
193
|
+
# second cancellation is delivered only AFTER TERM -> grace -> KILL ends.
|
|
194
|
+
with _cancellation_scope() as guard:
|
|
195
|
+
with guard.deferred():
|
|
196
|
+
_kill_group_impl(process)
|
|
197
|
+
|
|
198
|
+
def _spawn_owned(command,**kwargs):
|
|
199
|
+
process=None
|
|
200
|
+
try:
|
|
201
|
+
with _cancellation_scope() as guard:
|
|
202
|
+
with guard.deferred():
|
|
203
|
+
process=subprocess.Popen(command,start_new_session=True,**kwargs)
|
|
204
|
+
return process
|
|
205
|
+
except BaseException:
|
|
206
|
+
if process is not None:
|
|
207
|
+
try:_kill_group(process)
|
|
208
|
+
except _Cancellation:pass # Preserve original cancellation after cleanup.
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _bounded_setup(command, timeout=300):
|
|
213
|
+
with _cancellation_scope():
|
|
214
|
+
process=None
|
|
215
|
+
try:
|
|
216
|
+
process=_spawn_owned(command,stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
217
|
+
code = process.wait(timeout=timeout)
|
|
218
|
+
if code:raise CreatorError('dependency-setup-failed')
|
|
219
|
+
except _Cancellation:
|
|
220
|
+
raise
|
|
221
|
+
except KeyboardInterrupt:
|
|
222
|
+
raise _Cancellation(signal.SIGINT) from None
|
|
223
|
+
except subprocess.TimeoutExpired:
|
|
224
|
+
raise CreatorError('dependency-setup-interrupted') from None
|
|
225
|
+
finally:
|
|
226
|
+
if process is not None:_kill_group(process)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _outside_native_host(path):
|
|
230
|
+
path = _path(path,missing=True)
|
|
231
|
+
if path == Path.home():
|
|
232
|
+
raise CreatorError('native-home-runtime-forbidden')
|
|
233
|
+
for forbidden in (Path.home()/'.claude',Path.home()/'.codex',Path.home()/'.agents',Path.home()/'.config/devin'):
|
|
234
|
+
if path == forbidden or forbidden in path.parents:
|
|
235
|
+
raise CreatorError('native-host-runtime-forbidden')
|
|
236
|
+
return path
|
|
237
|
+
|
|
238
|
+
def setup(runtime_root, yes=False):
|
|
239
|
+
if not yes:
|
|
240
|
+
raise CreatorError('explicit-setup-yes-required')
|
|
241
|
+
if sys.version_info < (3,10):
|
|
242
|
+
raise CreatorError('native-donor-requires-python-3.10-or-later')
|
|
243
|
+
files = _donor_files() # Verify everything BEFORE any write.
|
|
244
|
+
root = _private_directory(_outside_native_host(runtime_root), create=True)
|
|
245
|
+
if any(root.iterdir()):
|
|
246
|
+
raise CreatorError('runtime-root-must-be-empty-no-overwrite')
|
|
247
|
+
# A failed setup remains visibly partial, never ready or implicitly repaired.
|
|
248
|
+
_write(root/'setup-state.json', json.dumps({'status':'partial','dependencies':DEPENDENCIES}).encode())
|
|
249
|
+
creator = root/'skill-creator'
|
|
250
|
+
creator.mkdir(mode=0o700)
|
|
251
|
+
for relative, raw in files.items():
|
|
252
|
+
destination = creator / ('SKILL.md' if relative == 'SKILL.md.source' else relative)
|
|
253
|
+
destination.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
254
|
+
_write(destination, raw)
|
|
255
|
+
_bounded_setup([sys.executable, '-m', 'venv', str(root/'venv')])
|
|
256
|
+
python = root/'venv/bin/python'
|
|
257
|
+
_bounded_setup([str(python), '-m', 'pip', 'install', '--disable-pip-version-check', *DEPENDENCIES])
|
|
258
|
+
_write(root/'ready.json', json.dumps({'schema':SCHEMA,'commit':COMMIT,'manifestSha256':MANIFEST_SHA256,
|
|
259
|
+
'dependencies':DEPENDENCIES,'pythonVersion':sys.version.split()[0],
|
|
260
|
+
'files':{('SKILL.md' if k=='SKILL.md.source' else k):_hash(v) for k,v in files.items()}}).encode())
|
|
261
|
+
return status(root)
|
|
262
|
+
|
|
263
|
+
def status(runtime_root):
|
|
264
|
+
root = _private_directory(_outside_native_host(runtime_root))
|
|
265
|
+
ready = root/'ready.json'
|
|
266
|
+
if not ready.exists():
|
|
267
|
+
return {'schema':SCHEMA,'status':'partial' if any(root.iterdir()) else 'not-installed','exitCode':1}
|
|
268
|
+
meta = json.loads(_read(ready, 1024*1024))
|
|
269
|
+
files = _donor_files()
|
|
270
|
+
expected = {('SKILL.md' if k=='SKILL.md.source' else k):_hash(v) for k,v in files.items()}
|
|
271
|
+
if meta.get('manifestSha256') != MANIFEST_SHA256 or meta.get('commit') != COMMIT or meta.get('files') != expected or meta.get('dependencies') != DEPENDENCIES:
|
|
272
|
+
raise CreatorError('runtime-metadata-mismatch')
|
|
273
|
+
for relative, digest in expected.items():
|
|
274
|
+
if _hash(_read(root/'skill-creator'/relative)) != digest:
|
|
275
|
+
raise CreatorError('runtime-source-hash-mismatch')
|
|
276
|
+
# venv's interpreter symlink is created by venv itself, not donor-controlled.
|
|
277
|
+
if not (root/'venv/bin/python').is_file():
|
|
278
|
+
raise CreatorError('runtime-python-missing')
|
|
279
|
+
return {'schema':SCHEMA,'status':'ready','exitCode':0,'dependencies':DEPENDENCIES,
|
|
280
|
+
'nativeHost':'Claude Code CLI','scripts':sorted(SCRIPTS),
|
|
281
|
+
'limitations':['Native Claude trigger evaluation, not Codex host equivalence.','Runtime and workspace are isolation conventions, not a sandbox.']}
|
|
282
|
+
|
|
283
|
+
def _option(args, name):
|
|
284
|
+
for i, arg in enumerate(args):
|
|
285
|
+
if arg == name:
|
|
286
|
+
if i+1 >= len(args):
|
|
287
|
+
raise CreatorError('missing-option-value')
|
|
288
|
+
return args[i+1]
|
|
289
|
+
if arg.startswith(name+'='):
|
|
290
|
+
return arg.split('=',1)[1]
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def _arguments(script, arguments):
|
|
294
|
+
args = list(arguments)
|
|
295
|
+
if script in ('run_eval','run_loop') and _option(args,'--num-workers') is None:
|
|
296
|
+
args += ['--num-workers','1']
|
|
297
|
+
if script == 'run_loop' and _option(args,'--report') is None:
|
|
298
|
+
args += ['--report','none']
|
|
299
|
+
return args
|
|
300
|
+
|
|
301
|
+
def _guard_skill(arguments, workspace):
|
|
302
|
+
value = _option(arguments,'--skill-path')
|
|
303
|
+
if value is None:
|
|
304
|
+
return
|
|
305
|
+
path = Path(value)
|
|
306
|
+
if not path.is_absolute():
|
|
307
|
+
path = workspace/path
|
|
308
|
+
raw = _read(_path(path)/'SKILL.md').decode('utf-8')
|
|
309
|
+
lines = raw.split('\n')
|
|
310
|
+
if not lines or lines[0].strip() != '---':
|
|
311
|
+
raise CreatorError('unsafe-native-skill-name')
|
|
312
|
+
end = next((i for i in range(1,len(lines)) if lines[i].strip()=='---'),None)
|
|
313
|
+
names = [line[len('name:'):].strip().strip('"').strip("'")
|
|
314
|
+
for line in lines[1:end] if line.startswith('name:')] if end is not None else []
|
|
315
|
+
if len(names) != 1 or not re.fullmatch(r'[a-z0-9][a-z0-9-]*',names[0]):
|
|
316
|
+
raise CreatorError('unsafe-native-skill-name')
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _guard_io(script,args,work):
|
|
320
|
+
def checked(value):
|
|
321
|
+
p=Path(value)
|
|
322
|
+
return _path(p if p.is_absolute() else work/p,missing=True)
|
|
323
|
+
for option in ('--eval-set','--skill-path','--eval-results','--history','--results-dir',
|
|
324
|
+
'--output','-o','--previous-workspace','--benchmark','--static','-s','--report'):
|
|
325
|
+
value=_option(args,option)
|
|
326
|
+
if value and not (option=='--report' and value in ('none','auto')):
|
|
327
|
+
checked(value)
|
|
328
|
+
if not args or args[0].startswith('-'):
|
|
329
|
+
return
|
|
330
|
+
if script in ('package_skill','quick_validate','aggregate_benchmark','generate_report','generate_review') and args[0]!='-':
|
|
331
|
+
source=checked(args[0])
|
|
332
|
+
if script=='package_skill':
|
|
333
|
+
output=checked(args[1]) if len(args)>1 else work
|
|
334
|
+
_path(output/(source.name+'.skill'),missing=True)
|
|
335
|
+
if source.is_dir():
|
|
336
|
+
for directory,dirs,files in os.walk(source,followlinks=False):
|
|
337
|
+
for name in dirs+files:
|
|
338
|
+
if (Path(directory)/name).is_symlink():
|
|
339
|
+
raise CreatorError('package-source-symlink-rejected')
|
|
340
|
+
if script=='aggregate_benchmark':
|
|
341
|
+
output=_option(args,'--output') or _option(args,'-o')
|
|
342
|
+
target=checked(output) if output else source/'benchmark.json'
|
|
343
|
+
_path(target,missing=True);_path(target.with_suffix('.md'),missing=True)
|
|
344
|
+
if script=='generate_review':
|
|
345
|
+
_path(source/'feedback.json',missing=True)
|
|
346
|
+
|
|
347
|
+
def _observed(value, integer=False):
|
|
348
|
+
try:
|
|
349
|
+
if type(value) not in (int,float) or not math.isfinite(value) or value < 0:
|
|
350
|
+
return None
|
|
351
|
+
except OverflowError:
|
|
352
|
+
return None
|
|
353
|
+
return value if not integer or type(value) is int else None
|
|
354
|
+
|
|
355
|
+
def _benchmark_stats(values):
|
|
356
|
+
# A partial mean would silently drop missing runs. Keep the whole statistic
|
|
357
|
+
# unknown; coverage separately records exactly how many metrics were observed.
|
|
358
|
+
if not values or any(value is None for value in values):
|
|
359
|
+
return None
|
|
360
|
+
try:
|
|
361
|
+
mean=sum(values)/len(values)
|
|
362
|
+
variance=sum((x-mean)**2 for x in values)/(len(values)-1) if len(values)>1 else 0
|
|
363
|
+
result={'mean':round(mean,4),'stddev':round(math.sqrt(variance),4),
|
|
364
|
+
'min':min(values),'max':max(values),'n':len(values)}
|
|
365
|
+
return result if all(math.isfinite(value) for value in result.values()) else None
|
|
366
|
+
except (OverflowError,ValueError):
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def corrected_benchmark(directory,skill_name='',skill_path=''):
|
|
370
|
+
"""Creator-owned adapter. Retains donor artifact shape, not its invented metrics."""
|
|
371
|
+
directory=Path(directory)
|
|
372
|
+
search=directory/'runs' if (directory/'runs').is_dir() else directory
|
|
373
|
+
rows=[];issues=[]
|
|
374
|
+
def data(path,required=False):
|
|
375
|
+
if not path.exists():
|
|
376
|
+
if required:issues.append('missing-grading-file')
|
|
377
|
+
return {}
|
|
378
|
+
try:
|
|
379
|
+
item=json.loads(_read(path))
|
|
380
|
+
if not isinstance(item,dict):raise ValueError()
|
|
381
|
+
return item
|
|
382
|
+
except (CreatorError,OSError,ValueError):
|
|
383
|
+
issues.append('unreadable-or-invalid-artifact')
|
|
384
|
+
return {}
|
|
385
|
+
for index,eval_dir in enumerate(sorted(search.glob('eval-*'))):
|
|
386
|
+
if not eval_dir.is_dir():continue
|
|
387
|
+
metadata=data(eval_dir/'eval_metadata.json')
|
|
388
|
+
fallback=int(eval_dir.name[5:]) if eval_dir.name[5:].isdigit() else index
|
|
389
|
+
eval_id=metadata.get('eval_id',fallback)
|
|
390
|
+
if type(eval_id) is not int:eval_id=fallback
|
|
391
|
+
for config in sorted(eval_dir.iterdir()):
|
|
392
|
+
if not config.is_dir():continue
|
|
393
|
+
for run_dir in sorted(config.glob('run-*')):
|
|
394
|
+
if not run_dir.is_dir():continue
|
|
395
|
+
grading=data(run_dir/'grading.json',True)
|
|
396
|
+
timing=data(run_dir/'timing.json')
|
|
397
|
+
grade_timing=grading.get('timing',{})
|
|
398
|
+
grade_timing=grade_timing if isinstance(grade_timing,dict) else {}
|
|
399
|
+
metrics=grading.get('execution_metrics',{})
|
|
400
|
+
metrics=metrics if isinstance(metrics,dict) else {}
|
|
401
|
+
summary=grading.get('summary',{})
|
|
402
|
+
summary=summary if isinstance(summary,dict) else {}
|
|
403
|
+
elapsed=_observed(grade_timing.get('total_duration_seconds'))
|
|
404
|
+
if elapsed is None:elapsed=_observed(timing.get('total_duration_seconds'))
|
|
405
|
+
tokens=_observed(timing.get('total_tokens'),True)
|
|
406
|
+
if tokens is None:tokens=_observed(metrics.get('total_tokens'),True)
|
|
407
|
+
rate=_observed(summary.get('pass_rate'))
|
|
408
|
+
if rate is not None and rate>1:rate=None
|
|
409
|
+
result={'pass_rate':rate,'passed':_observed(summary.get('passed'),True),
|
|
410
|
+
'failed':_observed(summary.get('failed'),True),'total':_observed(summary.get('total'),True),
|
|
411
|
+
'time_seconds':elapsed,'tokens':tokens,
|
|
412
|
+
'output_chars':_observed(metrics.get('output_chars'),True),
|
|
413
|
+
'tool_calls':_observed(metrics.get('total_tool_calls'),True),
|
|
414
|
+
'errors':_observed(metrics.get('errors_encountered'),True)}
|
|
415
|
+
number=run_dir.name[4:]
|
|
416
|
+
rows.append({'eval_id':eval_id,'configuration':config.name,
|
|
417
|
+
'run_number':int(number) if number.isdigit() else run_dir.name,'result':result,
|
|
418
|
+
'expectations':grading.get('expectations',[]),'notes':[]})
|
|
419
|
+
configs=sorted({row['configuration'] for row in rows})
|
|
420
|
+
evals=sorted({row['eval_id'] for row in rows})
|
|
421
|
+
counts={config:sum(row['configuration']==config for row in rows) for config in configs}
|
|
422
|
+
cells={str(e):{config:sum(row['eval_id']==e and row['configuration']==config for row in rows)
|
|
423
|
+
for config in configs} for e in evals}
|
|
424
|
+
cell_values=[count for item in cells.values() for count in item.values()]
|
|
425
|
+
uniform=cell_values[0] if cell_values and len(set(cell_values))==1 else None
|
|
426
|
+
summary={};coverage={}
|
|
427
|
+
for config in configs:
|
|
428
|
+
selected=[row for row in rows if row['configuration']==config]
|
|
429
|
+
summary[config]={};coverage[config]={}
|
|
430
|
+
for metric in ('pass_rate','time_seconds','tokens','output_chars'):
|
|
431
|
+
values=[row['result'][metric] for row in selected]
|
|
432
|
+
summary[config][metric]=_benchmark_stats(values)
|
|
433
|
+
observed=sum(value is not None for value in values)
|
|
434
|
+
coverage[config][metric]={'observed':observed,'missing':len(values)-observed}
|
|
435
|
+
delta={}
|
|
436
|
+
for metric,digits in (('pass_rate',2),('time_seconds',1),('tokens',0),('output_chars',0)):
|
|
437
|
+
a=summary[configs[0]][metric] if configs else None
|
|
438
|
+
b=summary[configs[1]][metric] if len(configs)>1 else None
|
|
439
|
+
delta[metric]=format(a['mean']-b['mean'],'+.'+str(digits)+'f') if a and b else 'unknown'
|
|
440
|
+
summary['delta']=delta
|
|
441
|
+
complete=bool(rows) and not issues and all(counts['missing']==0 for item in coverage.values()
|
|
442
|
+
for metric,counts in item.items() if metric in ('pass_rate','time_seconds','tokens')) and all(
|
|
443
|
+
summary[config][metric] is not None for config in configs for metric in ('pass_rate','time_seconds','tokens'))
|
|
444
|
+
return {'metadata':{'skill_name':skill_name or '<skill-name>','skill_path':skill_path or '<path/to/skill>',
|
|
445
|
+
'executor_model':None,'analyzer_model':None,'timestamp':datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
446
|
+
'evals_run':evals,'runs_per_configuration':uniform,'observed_run_counts':counts,
|
|
447
|
+
'observed_counts_by_eval':cells,'adapter':'crow-creator-truthful-benchmark/1'},
|
|
448
|
+
'runs':rows,'run_summary':summary,'coverage':{'complete':complete,'metrics':coverage,'issues':issues},
|
|
449
|
+
'notes':['Creator-owned corrected aggregation; donor source remains unchanged.',
|
|
450
|
+
'Unknown metrics are null, never zero. Output characters are not tokens.',
|
|
451
|
+
'Supplied grading and token fields are input evidence, not independent behavior/provider attestations.']}
|
|
452
|
+
|
|
453
|
+
def corrected_benchmark_markdown(benchmark):
|
|
454
|
+
metadata=benchmark['metadata'];summary=benchmark['run_summary']
|
|
455
|
+
configs=[key for key in summary if key!='delta']
|
|
456
|
+
lines=['# Skill Benchmark: '+metadata['skill_name'],'',
|
|
457
|
+
'**Model**: unknown (not inferred from artifacts)',
|
|
458
|
+
'**Date**: '+metadata['timestamp'],
|
|
459
|
+
'**Observed runs**: '+('; '.join(key+': '+str(value) for key,value in metadata['observed_run_counts'].items()) or 'none'),
|
|
460
|
+
'**Coverage**: '+('complete for reported metrics' if benchmark['coverage']['complete'] else 'incomplete; missing metrics remain unknown'),'',
|
|
461
|
+
'| Metric | '+' | '.join(key.replace('_',' ').title() for key in configs)+' | Delta |',
|
|
462
|
+
'|---|'+'---|'*(len(configs)+1)]
|
|
463
|
+
for metric,label in (('pass_rate','Pass Rate'),('time_seconds','Time (s)'),('tokens','Tokens'),('output_chars','Output Characters')):
|
|
464
|
+
values=[]
|
|
465
|
+
for config in configs:
|
|
466
|
+
item=summary[config][metric]
|
|
467
|
+
if item is None:values.append('unknown')
|
|
468
|
+
elif metric=='pass_rate':values.append(format(item['mean']*100,'.0f')+'% (n='+str(item['n'])+')')
|
|
469
|
+
else:values.append(format(item['mean'],'.4g')+' (n='+str(item['n'])+')')
|
|
470
|
+
lines.append('| '+label+' | '+' | '.join(values)+' | '+summary['delta'][metric]+' |')
|
|
471
|
+
lines += ['','## Notes','']+['- '+note for note in benchmark['notes']]
|
|
472
|
+
return '\n'.join(lines)
|
|
473
|
+
|
|
474
|
+
def _aggregate_entry(target,arguments):
|
|
475
|
+
namespace=runpy.run_path(target)
|
|
476
|
+
namespace['main'].__globals__.update(generate_benchmark=corrected_benchmark,
|
|
477
|
+
generate_markdown=corrected_benchmark_markdown)
|
|
478
|
+
sys.argv=[target,*arguments]
|
|
479
|
+
namespace['main']()
|
|
480
|
+
return 0
|
|
481
|
+
|
|
482
|
+
def _truthful_viewer_html(html):
|
|
483
|
+
"""Patch only reviewed pinned renderer snippets, leaving all viewer features."""
|
|
484
|
+
replacements = [
|
|
485
|
+
(' function renderBenchmark() {', ''' function renderBenchmark() {
|
|
486
|
+
// Crow adapter: missing observations stay unknown through rendering.
|
|
487
|
+
const crowKnown = v => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
488
|
+
const crowMean = values => values.length && values.every(crowKnown)
|
|
489
|
+
? values.reduce((a,b) => a+b, 0) / values.length : null;
|
|
490
|
+
const crowMeanText = (values, percent=false) => {
|
|
491
|
+
const mean = crowMean(values);
|
|
492
|
+
return mean == null || !Number.isFinite(mean) ? "unknown (incomplete)"
|
|
493
|
+
: (mean * (percent ? 100 : 1)).toFixed(percent ? 0 : 1) + (percent ? "%" : "");
|
|
494
|
+
};
|
|
495
|
+
'''),
|
|
496
|
+
('if (!stat) return "—";', 'if (!stat || !crowKnown(stat.mean) || !crowKnown(stat.stddev)) return "unknown";'),
|
|
497
|
+
('(metadata.runs_per_configuration || "?") + " runs per configuration"',
|
|
498
|
+
'(metadata.observed_run_counts ? escapeHtml(Object.entries(metadata.observed_run_counts).map(([k,v]) => k + ": " + v).join("; ")) + " observed runs" : (metadata.runs_per_configuration ?? "unknown") + " runs per configuration")'),
|
|
499
|
+
('if (a.time_seconds || b.time_seconds) {', 'if ("time_seconds" in a || "time_seconds" in b) {'),
|
|
500
|
+
('if (a.tokens || b.tokens) {', 'if ("tokens" in a || "tokens" in b) {'),
|
|
501
|
+
('(delta.time_seconds ? delta.time_seconds + "s" : "—")',
|
|
502
|
+
'(delta.time_seconds != null && Number.isFinite(Number(delta.time_seconds)) ? delta.time_seconds + "s" : "unknown")'),
|
|
503
|
+
('const hasTime = runs.some(r => r.result && r.result.time_seconds != null);',
|
|
504
|
+
'const hasTime = runs.some(r => r.result && "time_seconds" in r.result);'),
|
|
505
|
+
('const hasErrors = runs.some(r => r.result && r.result.errors > 0);',
|
|
506
|
+
'const hasErrors = runs.some(r => r.result && "errors" in r.result);'),
|
|
507
|
+
('const prClass = r.pass_rate >= 0.8 ? "benchmark-delta-positive" : r.pass_rate < 0.5 ? "benchmark-delta-negative" : "";',
|
|
508
|
+
'const prClass = crowKnown(r.pass_rate) ? (r.pass_rate >= 0.8 ? "benchmark-delta-positive" : r.pass_rate < 0.5 ? "benchmark-delta-negative" : "") : "";'),
|
|
509
|
+
('((r.pass_rate || 0) * 100).toFixed(0) + "% (" + (r.passed || 0) + "/" + (r.total || 0) + ")',
|
|
510
|
+
'(crowKnown(r.pass_rate) ? (r.pass_rate * 100).toFixed(0) + "%" : "unknown") + " (" + (crowKnown(r.passed) ? r.passed : "unknown") + "/" + (crowKnown(r.total) ? r.total : "unknown") + ")'),
|
|
511
|
+
('(r.time_seconds != null ? r.time_seconds.toFixed(1) : "—")',
|
|
512
|
+
'(crowKnown(r.time_seconds) ? r.time_seconds.toFixed(1) : "unknown")'),
|
|
513
|
+
('(r.errors || 0)', '(crowKnown(r.errors) ? r.errors : "unknown")'),
|
|
514
|
+
('const rates = configRuns.map(r => (r.result || {}).pass_rate || 0);',
|
|
515
|
+
'const rates = configRuns.map(r => (r.result || {}).pass_rate);'),
|
|
516
|
+
('const avgRate = rates.reduce((a, b) => a + b, 0) / rates.length;', 'const avgRate = crowMean(rates);'),
|
|
517
|
+
('const avgPrClass = avgRate >= 0.8 ? "benchmark-delta-positive" : avgRate < 0.5 ? "benchmark-delta-negative" : "";',
|
|
518
|
+
'const avgPrClass = avgRate == null ? "" : (avgRate >= 0.8 ? "benchmark-delta-positive" : avgRate < 0.5 ? "benchmark-delta-negative" : "");'),
|
|
519
|
+
('(avgRate * 100).toFixed(0) + "%', 'crowMeanText(rates, true) + "'),
|
|
520
|
+
('const times = configRuns.map(r => (r.result || {}).time_seconds).filter(t => t != null);',
|
|
521
|
+
'const times = configRuns.map(r => (r.result || {}).time_seconds);'),
|
|
522
|
+
('(times.length ? (times.reduce((a, b) => a + b, 0) / times.length).toFixed(1) : "—")',
|
|
523
|
+
'crowMeanText(times)'),
|
|
524
|
+
('const cls = exp.passed ? "benchmark-delta-positive" : "benchmark-delta-negative";',
|
|
525
|
+
'const cls = exp.passed === true ? "benchmark-delta-positive" : exp.passed === false ? "benchmark-delta-negative" : "";'),
|
|
526
|
+
('const icon = exp.passed ? "\\u2713" : "\\u2717";',
|
|
527
|
+
'const icon = exp.passed === true ? "\\u2713" : exp.passed === false ? "\\u2717" : "unknown";'),
|
|
528
|
+
]
|
|
529
|
+
for original, replacement in replacements:
|
|
530
|
+
if html.count(original) != 1:
|
|
531
|
+
raise CreatorError('pinned-viewer-adapter-target-changed')
|
|
532
|
+
html=html.replace(original,replacement,1)
|
|
533
|
+
return html
|
|
534
|
+
|
|
535
|
+
def _viewer_namespace(target):
|
|
536
|
+
namespace=runpy.run_path(target)
|
|
537
|
+
globals_=namespace['main'].__globals__
|
|
538
|
+
original=globals_['generate_html']
|
|
539
|
+
def render(*args,**kwargs):
|
|
540
|
+
return _truthful_viewer_html(original(*args,**kwargs))
|
|
541
|
+
# Static main and served ReviewHandler share these module globals.
|
|
542
|
+
globals_.update(generate_html=render,_kill_port=lambda port:None)
|
|
543
|
+
return namespace
|
|
544
|
+
|
|
545
|
+
def _viewer_entry(target,arguments):
|
|
546
|
+
namespace=_viewer_namespace(target)
|
|
547
|
+
sys.argv=[target,*arguments]
|
|
548
|
+
namespace['main']()
|
|
549
|
+
return 0
|
|
550
|
+
|
|
551
|
+
def _event(path, value):
|
|
552
|
+
# Readers only glob final event suffixes; publish fully written bytes once.
|
|
553
|
+
temp=path.with_name('.event-'+uuid.uuid4().hex)
|
|
554
|
+
try:
|
|
555
|
+
_write(temp,json.dumps(value).encode())
|
|
556
|
+
os.replace(temp,path)
|
|
557
|
+
finally:
|
|
558
|
+
if temp.exists():temp.unlink()
|
|
559
|
+
|
|
560
|
+
def _traces(directory):
|
|
561
|
+
starts = list(directory.glob('*.start'))
|
|
562
|
+
done, failures, early, invalid, protocol_failures, protocol_incomplete = 0, 0, 0, 0, 0, 0
|
|
563
|
+
for start in starts:
|
|
564
|
+
path = start.with_suffix('.done')
|
|
565
|
+
if path.exists():
|
|
566
|
+
try:
|
|
567
|
+
item = json.loads(_read(path,4096))
|
|
568
|
+
if not isinstance(item,dict) or not (type(item.get('returncode')) is int or
|
|
569
|
+
(item.get('returncode') is None and isinstance(item.get('error'),str) and item['error'])):
|
|
570
|
+
raise CreatorError('invalid-final-trace')
|
|
571
|
+
except (OSError,CreatorError,ValueError):
|
|
572
|
+
invalid += 1
|
|
573
|
+
continue
|
|
574
|
+
done += 1
|
|
575
|
+
failures += item.get('returncode') != 0 or bool(item.get('error'))
|
|
576
|
+
early += bool(item.get('consumerClosedEarly'))
|
|
577
|
+
protocol=item.get('protocol',{})
|
|
578
|
+
if isinstance(protocol,dict) and protocol.get('status')=='error':protocol_failures+=1
|
|
579
|
+
elif not isinstance(protocol,dict) or protocol.get('status')!='success':protocol_incomplete+=1
|
|
580
|
+
return {'started':len(starts),'completed':done,'cliFailures':failures,'pending':len(starts)-done,
|
|
581
|
+
'actualStarted':len(list(directory.glob('*.running'))),'earlyConsumerClosed':early,'invalidFinalEvents':invalid,'protocolFailures':protocol_failures,'protocolIncomplete':protocol_incomplete}
|
|
582
|
+
|
|
583
|
+
def _acquire_native_slot(directory, workers):
|
|
584
|
+
# Locks are scoped to this run and held until the REAL CLI exits, not until
|
|
585
|
+
# its disposable donor-facing proxy is deliberately killed on early output.
|
|
586
|
+
while True:
|
|
587
|
+
for index in range(workers):
|
|
588
|
+
fd=os.open(Path(directory)/('slot-'+str(index)),os.O_RDWR|os.O_CREAT|os.O_NOFOLLOW,0o600)
|
|
589
|
+
try:
|
|
590
|
+
fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
|
591
|
+
return fd
|
|
592
|
+
except BlockingIOError:
|
|
593
|
+
os.close(fd)
|
|
594
|
+
time.sleep(.025)
|
|
595
|
+
|
|
596
|
+
class _ProtocolEvidence:
|
|
597
|
+
"""Bounded stream evidence, not an OS-exit-code surrogate or behavior grader."""
|
|
598
|
+
def __init__(self,format_):
|
|
599
|
+
self.format=format_;self.buffer=b'';self.discarding=False
|
|
600
|
+
self.malformed=0;self.errors=0;self.successes=0;self.nonempty=False
|
|
601
|
+
self.max_line=4*1024*1024
|
|
602
|
+
def _line(self,line):
|
|
603
|
+
if not line.strip():return
|
|
604
|
+
if len(line)>self.max_line:
|
|
605
|
+
self.malformed+=1;return
|
|
606
|
+
try:
|
|
607
|
+
def invalid_constant(_):raise ValueError('non-json-number')
|
|
608
|
+
event=json.loads(line,parse_constant=invalid_constant)
|
|
609
|
+
except (ValueError,UnicodeError,RecursionError):
|
|
610
|
+
self.malformed+=1;return
|
|
611
|
+
if not isinstance(event,dict) or not isinstance(event.get('type'),str):
|
|
612
|
+
self.malformed+=1;return
|
|
613
|
+
if event['type']=='error':self.errors+=1
|
|
614
|
+
if event['type']=='result':
|
|
615
|
+
if event.get('is_error') is True or str(event.get('subtype','')).startswith('error') or event.get('errors'):
|
|
616
|
+
self.errors+=1
|
|
617
|
+
elif event.get('subtype')=='success' and event.get('is_error') is False:
|
|
618
|
+
self.successes+=1
|
|
619
|
+
def feed(self,raw):
|
|
620
|
+
self.nonempty=self.nonempty or bool(raw.strip())
|
|
621
|
+
if self.format!='stream-json':return
|
|
622
|
+
if self.discarding:
|
|
623
|
+
boundary=raw.find(b'\n')
|
|
624
|
+
if boundary<0:return
|
|
625
|
+
raw=raw[boundary+1:];self.discarding=False
|
|
626
|
+
self.buffer+=raw
|
|
627
|
+
while b'\n' in self.buffer:
|
|
628
|
+
line,_,self.buffer=self.buffer.partition(b'\n');self._line(line)
|
|
629
|
+
if len(self.buffer)>self.max_line:
|
|
630
|
+
self.malformed+=1;self.buffer=b'';self.discarding=True
|
|
631
|
+
def finish(self):
|
|
632
|
+
if self.format=='stream-json':
|
|
633
|
+
if self.buffer.strip():self._line(self.buffer)
|
|
634
|
+
self.buffer=b''
|
|
635
|
+
state='error' if self.errors or self.malformed or self.successes>1 else 'success' if self.successes==1 else 'incomplete'
|
|
636
|
+
elif self.format=='text':state='success' if self.nonempty else 'incomplete'
|
|
637
|
+
else:state='incomplete'
|
|
638
|
+
return {'format':self.format,'status':state,'terminalSuccesses':self.successes,
|
|
639
|
+
'errorEvents':self.errors,'malformedEvents':self.malformed,'nonemptyText':self.nonempty}
|
|
640
|
+
|
|
641
|
+
def _supervise(real, trace):
|
|
642
|
+
token = uuid.uuid4().hex
|
|
643
|
+
start, done = Path(trace)/(token+'.start'), Path(trace)/(token+'.done')
|
|
644
|
+
_event(start,{})
|
|
645
|
+
result = {'returncode':None,'consumerClosedEarly':False}
|
|
646
|
+
protocol=_ProtocolEvidence(_option(sys.argv[4:],'--output-format') or 'text')
|
|
647
|
+
slot = None
|
|
648
|
+
try:
|
|
649
|
+
slot = _acquire_native_slot(os.environ['CROW_CREATOR_SLOTS'],int(os.environ['CROW_CREATOR_WORKERS']))
|
|
650
|
+
_event(Path(trace)/(token+'.running'),{})
|
|
651
|
+
process = subprocess.Popen([real,*sys.argv[4:]], stdout=subprocess.PIPE)
|
|
652
|
+
forwarding = True
|
|
653
|
+
while True:
|
|
654
|
+
raw = os.read(process.stdout.fileno(),65536)
|
|
655
|
+
if not raw:
|
|
656
|
+
break
|
|
657
|
+
protocol.feed(raw)
|
|
658
|
+
if forwarding:
|
|
659
|
+
try:
|
|
660
|
+
offset = 0
|
|
661
|
+
while offset < len(raw):
|
|
662
|
+
offset += os.write(1,raw[offset:])
|
|
663
|
+
except BrokenPipeError:
|
|
664
|
+
forwarding = False # Native donor intentionally stops early.
|
|
665
|
+
result['consumerClosedEarly'] = True
|
|
666
|
+
result['returncode'] = process.wait()
|
|
667
|
+
except (OSError, KeyboardInterrupt):
|
|
668
|
+
result['error'] = 'cli-start-or-supervisor-failure'
|
|
669
|
+
finally:
|
|
670
|
+
result['protocol']=protocol.finish()
|
|
671
|
+
_event(done,result)
|
|
672
|
+
if slot is not None:
|
|
673
|
+
os.close(slot)
|
|
674
|
+
return result['returncode'] if result['returncode'] is not None else 125
|
|
675
|
+
|
|
676
|
+
def _proxy():
|
|
677
|
+
real, trace = os.environ['CROW_CREATOR_REAL_CLAUDE'], os.environ['CROW_CREATOR_TRACE']
|
|
678
|
+
# Supervisor survives donor's deliberate kill of this proxy; caller owns group.
|
|
679
|
+
process = subprocess.Popen([sys.executable, str(Path(__file__).absolute()), '__supervise',real,trace,*sys.argv[2:]])
|
|
680
|
+
return process.wait()
|
|
681
|
+
|
|
682
|
+
def run(script, runtime_root, workspace, timeout, arguments=(), stdio=False, interactive=False):
|
|
683
|
+
if script not in SCRIPTS or type(timeout) not in (int,float) or not math.isfinite(timeout) or timeout <= 0:
|
|
684
|
+
raise CreatorError('invalid-script-or-timeout')
|
|
685
|
+
root = _private_directory(runtime_root)
|
|
686
|
+
if status(root)['status'] != 'ready':
|
|
687
|
+
raise CreatorError('runtime-not-ready-explicit-setup-required')
|
|
688
|
+
work = _private_directory(workspace, create=True)
|
|
689
|
+
if work == Path.home() or work == root or root in work.parents or work in root.parents:
|
|
690
|
+
raise CreatorError('workspace-must-be-separate')
|
|
691
|
+
for forbidden in (Path.home()/'.claude',Path.home()/'.codex',Path.home()/'.agents'):
|
|
692
|
+
if work == forbidden or forbidden in work.parents:
|
|
693
|
+
raise CreatorError('native-host-workspace-forbidden')
|
|
694
|
+
local = work/'.claude'
|
|
695
|
+
_private_directory(local, create=True)
|
|
696
|
+
commands=_private_directory(local/'commands', create=True)
|
|
697
|
+
if any(p.name!='commands' for p in local.iterdir()) or any(commands.iterdir()):
|
|
698
|
+
raise CreatorError('workspace-claude-must-be-unconfigured-and-empty')
|
|
699
|
+
arguments = _arguments(script,arguments)
|
|
700
|
+
_guard_io(script,arguments,work)
|
|
701
|
+
if script in ('run_eval','run_loop','improve_description'):
|
|
702
|
+
_guard_skill(arguments,work)
|
|
703
|
+
if script == 'generate_review' and not any(a in ('--help','-h') for a in arguments):
|
|
704
|
+
static_viewer = _option(arguments,'--static') or _option(arguments,'-s')
|
|
705
|
+
if not static_viewer and not interactive:
|
|
706
|
+
raise CreatorError('viewer-requires-static-or-explicit-interactive')
|
|
707
|
+
run_dir = Path(tempfile.mkdtemp(prefix='.creator-run-',dir=work))
|
|
708
|
+
run_dir.chmod(0o700)
|
|
709
|
+
trace = run_dir/'trace'; trace.mkdir(mode=0o700)
|
|
710
|
+
slots = run_dir/'slots'; slots.mkdir(mode=0o700)
|
|
711
|
+
try:
|
|
712
|
+
workers = int(_option(arguments,'--num-workers') or '1')
|
|
713
|
+
except ValueError:
|
|
714
|
+
raise CreatorError('num-workers-must-be-positive-integer') from None
|
|
715
|
+
if workers < 1:
|
|
716
|
+
raise CreatorError('num-workers-must-be-positive-integer')
|
|
717
|
+
bin_dir = run_dir/'bin'; bin_dir.mkdir(mode=0o700)
|
|
718
|
+
env = os.environ.copy()
|
|
719
|
+
env.pop('PYTHONHOME',None)
|
|
720
|
+
env['PYTHONPATH'] = str(root/'skill-creator')
|
|
721
|
+
env['PYTHONDONTWRITEBYTECODE'] = '1'
|
|
722
|
+
is_native = script in ('run_eval','run_loop','improve_description') and '--help' not in arguments and '-h' not in arguments
|
|
723
|
+
if is_native:
|
|
724
|
+
real = shutil.which('claude')
|
|
725
|
+
if not real:
|
|
726
|
+
raise CreatorError('claude-cli-not-found')
|
|
727
|
+
real = str(Path(real).resolve())
|
|
728
|
+
if env.get('CROW_CREATOR_PROXY') == '1' or real == str(bin_dir/'claude'):
|
|
729
|
+
raise CreatorError('recursive-claude-proxy')
|
|
730
|
+
shim = '#!'+str(root/'venv/bin/python')+'\nimport runpy,sys\nsys.argv=['+repr(str(Path(__file__).absolute()))+",'__proxy',*sys.argv[1:]]\nrunpy.run_path(sys.argv[0],run_name='__main__')\n"
|
|
731
|
+
_write(bin_dir/'claude',shim.encode(),0o700)
|
|
732
|
+
env.update(PATH=str(bin_dir)+os.pathsep+env.get('PATH',''), CROW_CREATOR_PROXY='1',
|
|
733
|
+
CROW_CREATOR_REAL_CLAUDE=real,CROW_CREATOR_TRACE=str(trace),
|
|
734
|
+
CROW_CREATOR_SLOTS=str(slots),CROW_CREATOR_WORKERS=str(workers))
|
|
735
|
+
target = root/'skill-creator'/SCRIPTS[script]
|
|
736
|
+
command = [str(root/'venv/bin/python'),str(target),*arguments]
|
|
737
|
+
if script == 'aggregate_benchmark':
|
|
738
|
+
command=[str(root/'venv/bin/python'),str(Path(__file__).absolute()),'__aggregate',str(target),*arguments]
|
|
739
|
+
if script == 'generate_review':
|
|
740
|
+
command=[str(root/'venv/bin/python'),str(Path(__file__).absolute()),'__viewer',str(target),*arguments]
|
|
741
|
+
stdout_path, stderr_path = run_dir/'stdout.log', run_dir/'stderr.log'
|
|
742
|
+
started = time.monotonic(); state = 'completed'; process = None
|
|
743
|
+
cancellation=None;selector=None
|
|
744
|
+
old_umask = os.umask(0o077)
|
|
745
|
+
try:
|
|
746
|
+
with _cancellation_scope():
|
|
747
|
+
try:
|
|
748
|
+
with os.fdopen(os.open(stdout_path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600),'wb') as out, \
|
|
749
|
+
os.fdopen(os.open(stderr_path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600),'wb') as err:
|
|
750
|
+
process=_spawn_owned(command,cwd=work,env=env,stdin=None if (stdio or interactive) else subprocess.DEVNULL,
|
|
751
|
+
stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
|
752
|
+
selector = selectors.DefaultSelector()
|
|
753
|
+
for pipe,stream,fd in ((process.stdout,out,1),(process.stderr,err,2)):
|
|
754
|
+
selector.register(pipe,selectors.EVENT_READ,(stream,fd))
|
|
755
|
+
while True:
|
|
756
|
+
if time.monotonic()-started >= timeout:
|
|
757
|
+
state = 'timeout'; break
|
|
758
|
+
for key,_ in selector.select(.05):
|
|
759
|
+
raw = os.read(key.fileobj.fileno(),65536)
|
|
760
|
+
if not raw:
|
|
761
|
+
selector.unregister(key.fileobj);continue
|
|
762
|
+
stream,fd = key.data;stream.write(raw);stream.flush()
|
|
763
|
+
if stdio or interactive:
|
|
764
|
+
try:os.write(fd,raw)
|
|
765
|
+
except BrokenPipeError:pass
|
|
766
|
+
if process.poll() is not None and not _traces(trace)['pending'] and not selector.get_map():
|
|
767
|
+
break
|
|
768
|
+
except KeyboardInterrupt as interruption:
|
|
769
|
+
state='interrupted'
|
|
770
|
+
cancellation=interruption.signum if isinstance(interruption,_Cancellation) else signal.SIGINT
|
|
771
|
+
finally:
|
|
772
|
+
if process is not None:
|
|
773
|
+
try:_kill_group(process)
|
|
774
|
+
except _Cancellation as late:
|
|
775
|
+
state='interrupted';cancellation=cancellation or late.signum
|
|
776
|
+
finally:
|
|
777
|
+
process.stdout.close();process.stderr.close()
|
|
778
|
+
if selector is not None:selector.close()
|
|
779
|
+
finally:
|
|
780
|
+
os.umask(old_umask)
|
|
781
|
+
traces = _traces(trace)
|
|
782
|
+
code = process.returncode if process is not None else None
|
|
783
|
+
if state == 'completed':
|
|
784
|
+
if code or traces['cliFailures'] or traces.get('protocolFailures',0):state='operational-failure'
|
|
785
|
+
elif traces['pending'] or traces.get('protocolIncomplete',0) or (is_native and traces['started']==0):state='incomplete-cli-evidence'
|
|
786
|
+
result = {'schema':SCHEMA,'status':state,'exitCode':128+cancellation if cancellation else 0 if state=='completed' else 1,
|
|
787
|
+
'timedOut':state=='timeout','cancellationSignal':signal.Signals(cancellation).name if cancellation else None,
|
|
788
|
+
'executionKind':'help-only' if '--help' in arguments or '-h' in arguments else ('native-Claude' if is_native else 'offline-utility'),
|
|
789
|
+
'upstreamExitCode':code,'nativeHost':'Claude Code CLI' if is_native else 'offline-upstream-utility',
|
|
790
|
+
'integrationAdapter':'truthful-benchmark/1' if script=='aggregate_benchmark' else None,
|
|
791
|
+
'effectiveNativeWorkers':workers,'script':script,'stdoutPath':str(stdout_path),'stderrPath':str(stderr_path),'trace':traces,
|
|
792
|
+
'elapsedSeconds':round(time.monotonic()-started,6),
|
|
793
|
+
'limitations':['Donor output is not certified when CLI evidence fails or is incomplete.',
|
|
794
|
+
'Native Claude trigger behavior is not Codex host equivalence.','Not a sandbox; explicit upstream arguments retain upstream effects.',
|
|
795
|
+
'Viewer never kills an existing port occupant; process group is cleaned on completion/timeout/interrupt.']}
|
|
796
|
+
_write(run_dir/'result.json',json.dumps(result).encode())
|
|
797
|
+
return result
|
|
798
|
+
|
|
799
|
+
def main(argv=None):
|
|
800
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
801
|
+
if argv and argv[0] == '__viewer':
|
|
802
|
+
return _viewer_entry(argv[1],argv[2:])
|
|
803
|
+
if argv and argv[0] == '__aggregate':
|
|
804
|
+
return _aggregate_entry(argv[1],argv[2:])
|
|
805
|
+
if argv and argv[0] == '__proxy':
|
|
806
|
+
return _proxy()
|
|
807
|
+
if argv and argv[0] == '__supervise':
|
|
808
|
+
return _supervise(argv[1],argv[2])
|
|
809
|
+
donor = []
|
|
810
|
+
if '--' in argv:
|
|
811
|
+
index = argv.index('--');donor=argv[index+1:];argv=argv[:index]
|
|
812
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
813
|
+
subs = parser.add_subparsers(dest='action',required=True)
|
|
814
|
+
for name in ('setup','status','run'):
|
|
815
|
+
sub = subs.add_parser(name)
|
|
816
|
+
sub.add_argument('--runtime-root',required=True)
|
|
817
|
+
if name=='setup':sub.add_argument('--yes',action='store_true')
|
|
818
|
+
if name=='run':
|
|
819
|
+
sub.add_argument('script',choices=sorted(SCRIPTS))
|
|
820
|
+
sub.add_argument('--workspace',required=True)
|
|
821
|
+
sub.add_argument('--timeout',type=float,required=True)
|
|
822
|
+
sub.add_argument('--stdio',action='store_true',help='Forward stdin and tee stdout/stderr; logs remain private.')
|
|
823
|
+
sub.add_argument('--interactive',action='store_true',help='Explicit viewer/browser/server operation within the caller timeout.')
|
|
824
|
+
args = parser.parse_args(argv)
|
|
825
|
+
try:
|
|
826
|
+
with _cancellation_scope():
|
|
827
|
+
if args.action=='setup':result=setup(args.runtime_root,args.yes)
|
|
828
|
+
elif args.action=='status':result=status(args.runtime_root)
|
|
829
|
+
else:result=run(args.script,args.runtime_root,args.workspace,args.timeout,donor,args.stdio,args.interactive)
|
|
830
|
+
except _Cancellation as cancellation:
|
|
831
|
+
result={'schema':SCHEMA,'status':'interrupted','exitCode':128+cancellation.signum,
|
|
832
|
+
'cancellationSignal':signal.Signals(cancellation.signum).name,'timedOut':False}
|
|
833
|
+
except (CreatorError,OSError,ValueError,TypeError,KeyError):
|
|
834
|
+
result={'schema':SCHEMA,'status':'operational-error','exitCode':2,'error':'unsafe-unavailable-or-incomplete-runtime-input'}
|
|
835
|
+
print(json.dumps(result,sort_keys=True,ensure_ascii=True))
|
|
836
|
+
return result['exitCode']
|
|
837
|
+
|
|
838
|
+
if __name__=='__main__':
|
|
839
|
+
sys.exit(main())
|