@riddledc/riddle-proof 0.8.30 → 0.8.32
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/dist/advanced/engine-harness.cjs +132 -10
- package/dist/advanced/engine-harness.js +2 -2
- package/dist/advanced/index.cjs +132 -10
- package/dist/advanced/index.d.cts +2 -2
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +4 -4
- package/dist/advanced/proof-run-core.cjs +3 -1
- package/dist/advanced/proof-run-core.d.cts +1 -1
- package/dist/advanced/proof-run-core.d.ts +1 -1
- package/dist/advanced/proof-run-core.js +1 -1
- package/dist/advanced/proof-run-engine.cjs +80 -1
- package/dist/advanced/proof-run-engine.d.cts +2 -2
- package/dist/advanced/proof-run-engine.d.ts +2 -2
- package/dist/advanced/proof-run-engine.js +2 -2
- package/dist/advanced/runner.js +2 -2
- package/dist/{chunk-3OTO7IDH.js → chunk-C2NHHBFV.js} +1 -1
- package/dist/{chunk-32RE64IO.js → chunk-IOI6QR3B.js} +78 -1
- package/dist/{chunk-XJA2GDVN.js → chunk-U73JPBZW.js} +1 -1
- package/dist/{chunk-K6HZUSHH.js → chunk-X7SQTCIQ.js} +3 -1
- package/dist/{chunk-UWO4YR7I.js → chunk-ZREWMTFA.js} +53 -10
- package/dist/cli/index.js +3 -3
- package/dist/cli.cjs +132 -10
- package/dist/cli.js +3 -3
- package/dist/engine-harness.cjs +132 -10
- package/dist/engine-harness.js +2 -2
- package/dist/index.cjs +132 -10
- package/dist/index.js +3 -3
- package/dist/{proof-run-core-C8FDUhle.d.cts → proof-run-core-B1GeqkR8.d.cts} +2 -0
- package/dist/{proof-run-core-C8FDUhle.d.ts → proof-run-core-B1GeqkR8.d.ts} +2 -0
- package/dist/proof-run-core.cjs +3 -1
- package/dist/proof-run-core.d.cts +1 -1
- package/dist/proof-run-core.d.ts +1 -1
- package/dist/proof-run-core.js +1 -1
- package/dist/{proof-run-engine-By7oLsF-.d.ts → proof-run-engine-DYfmd8d7.d.ts} +4 -4
- package/dist/{proof-run-engine-D80hVFMf.d.cts → proof-run-engine-DeHxtGnW.d.cts} +4 -4
- package/dist/proof-run-engine.cjs +80 -1
- package/dist/proof-run-engine.d.cts +2 -2
- package/dist/proof-run-engine.d.ts +2 -2
- package/dist/proof-run-engine.js +2 -2
- package/dist/runner.js +2 -2
- package/lib/workspace-core.mjs +62 -7
- package/package.json +2 -2
- package/runtime/lib/riddle_core_call.mjs +662 -40
- package/runtime/lib/ship.py +363 -16
- package/runtime/lib/util.py +117 -40
- package/runtime/lib/verify.py +4 -3
- package/runtime/pipelines/riddle-proof-ship.lobster +11 -1
- package/runtime/tests/recon_verify_smoke.py +132 -0
- package/runtime/tests/ship_artifact_publication.py +185 -0
package/runtime/lib/ship.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Ship: commit, create PR, post proof artifacts, wait for CI, mark ready, cleanup."""
|
|
2
2
|
|
|
3
|
-
import json, subprocess as sp, time, os, sys, re
|
|
3
|
+
import json, subprocess as sp, time, os, sys, re, shutil, tempfile, hashlib
|
|
4
4
|
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
5
6
|
import urllib.request
|
|
6
7
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
7
8
|
from util import load_state, save_state, invoke, git
|
|
@@ -9,6 +10,7 @@ from util import load_state, save_state, invoke, git
|
|
|
9
10
|
|
|
10
11
|
DISCORD_API = 'https://discord.com/api/v10'
|
|
11
12
|
SHIP_NOISE_PATHS = ('.codex', '.oc-smoke')
|
|
13
|
+
MAX_GITHUB_PROOF_ARTIFACT_BYTES = 10 * 1024 * 1024
|
|
12
14
|
VISUAL_FIRST_MODES = {
|
|
13
15
|
'visual', 'render', 'ui', 'layout', 'screenshot',
|
|
14
16
|
'canvas', 'animation',
|
|
@@ -185,6 +187,306 @@ def truthy(value):
|
|
|
185
187
|
return str(value or '').strip().lower() in ('1', 'true', 'yes', 'y', 'on')
|
|
186
188
|
|
|
187
189
|
|
|
190
|
+
def safe_slug(value, fallback='artifact'):
|
|
191
|
+
slug = re.sub(r'[^a-zA-Z0-9._-]+', '-', str(value or '').strip()).strip('-._')
|
|
192
|
+
return slug[:80] or fallback
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_http_url(value):
|
|
196
|
+
text = str(value or '').strip()
|
|
197
|
+
return text.startswith('https://') or text.startswith('http://')
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def is_file_url(value):
|
|
201
|
+
return str(value or '').strip().startswith('file://')
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def path_from_file_url(url):
|
|
205
|
+
parsed = urllib.parse.urlparse(str(url or '').strip())
|
|
206
|
+
if parsed.scheme != 'file':
|
|
207
|
+
return ''
|
|
208
|
+
return urllib.request.url2pathname(parsed.path)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def local_path_from_artifact(artifact):
|
|
212
|
+
if not isinstance(artifact, dict):
|
|
213
|
+
return ''
|
|
214
|
+
path_value = str(artifact.get('path') or '').strip()
|
|
215
|
+
if path_value and os.path.exists(path_value):
|
|
216
|
+
return path_value
|
|
217
|
+
url = str(artifact.get('url') or '').strip()
|
|
218
|
+
if is_file_url(url):
|
|
219
|
+
decoded = path_from_file_url(url)
|
|
220
|
+
if decoded and os.path.exists(decoded):
|
|
221
|
+
return decoded
|
|
222
|
+
return ''
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def artifact_kind(name, path_value='', url=''):
|
|
226
|
+
target = (str(name or '') + ' ' + str(path_value or '') + ' ' + str(url or '')).lower()
|
|
227
|
+
if re.search(r'\.(png|jpe?g|gif|webp|avif|svg)(\?|$|\s)', target):
|
|
228
|
+
return 'image'
|
|
229
|
+
if re.search(r'\.(json|har|txt|md|html|log)(\?|$|\s)', target):
|
|
230
|
+
return 'data'
|
|
231
|
+
return 'artifact'
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def artifact_name(name, path_value='', fallback='artifact'):
|
|
235
|
+
source = str(name or '').strip() or os.path.basename(str(path_value or '').strip()) or fallback
|
|
236
|
+
source = source.split('?', 1)[0].split('#', 1)[0]
|
|
237
|
+
return safe_slug(source, fallback)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def add_artifact_source(sources, seen, role, name='', url='', path_value='', source='state'):
|
|
241
|
+
local_path = path_value if path_value and os.path.exists(path_value) else ''
|
|
242
|
+
if not local_path and is_file_url(url):
|
|
243
|
+
local_path = path_from_file_url(url)
|
|
244
|
+
key = local_path or str(url or '').strip()
|
|
245
|
+
if not key or key in seen:
|
|
246
|
+
return
|
|
247
|
+
seen.add(key)
|
|
248
|
+
kind = artifact_kind(name, local_path, url)
|
|
249
|
+
sources.append({
|
|
250
|
+
'role': role,
|
|
251
|
+
'name': artifact_name(name, local_path, role + '-artifact'),
|
|
252
|
+
'url': str(url or '').strip(),
|
|
253
|
+
'path': local_path,
|
|
254
|
+
'kind': kind,
|
|
255
|
+
'source': source,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def collect_proof_artifact_sources(state):
|
|
260
|
+
sources = []
|
|
261
|
+
seen = set()
|
|
262
|
+
for role, key in (('before', 'before_cdn'), ('prod', 'prod_cdn'), ('after', 'after_cdn')):
|
|
263
|
+
value = str(state.get(key) or '').strip()
|
|
264
|
+
if value:
|
|
265
|
+
add_artifact_source(sources, seen, role, os.path.basename(path_from_file_url(value)) if is_file_url(value) else role + '.png', value, '', key)
|
|
266
|
+
|
|
267
|
+
verify_results = state.get('verify_results') or {}
|
|
268
|
+
after = verify_results.get('after') if isinstance(verify_results, dict) else {}
|
|
269
|
+
raw = after.get('raw') if isinstance(after, dict) else {}
|
|
270
|
+
if isinstance(raw, dict):
|
|
271
|
+
for artifact in raw.get('outputs') or []:
|
|
272
|
+
if isinstance(artifact, dict):
|
|
273
|
+
add_artifact_source(sources, seen, 'after', artifact.get('name', ''), artifact.get('url', ''), local_path_from_artifact(artifact), 'verify_results.after.raw.outputs')
|
|
274
|
+
for artifact in raw.get('screenshots') or []:
|
|
275
|
+
if isinstance(artifact, dict):
|
|
276
|
+
add_artifact_source(sources, seen, 'after', artifact.get('name', ''), artifact.get('url', ''), local_path_from_artifact(artifact), 'verify_results.after.raw.screenshots')
|
|
277
|
+
|
|
278
|
+
bundle = state.get('evidence_bundle') or {}
|
|
279
|
+
bundle_after = bundle.get('after') if isinstance(bundle, dict) else {}
|
|
280
|
+
supporting = bundle_after.get('supporting_artifacts') if isinstance(bundle_after, dict) else {}
|
|
281
|
+
if isinstance(supporting, dict):
|
|
282
|
+
for field, kind_role in (('image_outputs', 'after'), ('data_outputs', 'after'), ('other_outputs', 'after')):
|
|
283
|
+
for artifact in supporting.get(field) or []:
|
|
284
|
+
if isinstance(artifact, dict):
|
|
285
|
+
add_artifact_source(sources, seen, kind_role, artifact.get('name', ''), artifact.get('url', ''), local_path_from_artifact(artifact), 'evidence_bundle.after.supporting_artifacts.' + field)
|
|
286
|
+
|
|
287
|
+
request = state.get('proof_assessment_request') or {}
|
|
288
|
+
request_supporting = request.get('supporting_artifacts') if isinstance(request, dict) else {}
|
|
289
|
+
if isinstance(request_supporting, dict):
|
|
290
|
+
for field, kind_role in (('image_outputs', 'after'), ('data_outputs', 'after'), ('other_outputs', 'after')):
|
|
291
|
+
for artifact in request_supporting.get(field) or []:
|
|
292
|
+
if isinstance(artifact, dict):
|
|
293
|
+
add_artifact_source(sources, seen, kind_role, artifact.get('name', ''), artifact.get('url', ''), local_path_from_artifact(artifact), 'proof_assessment_request.supporting_artifacts.' + field)
|
|
294
|
+
|
|
295
|
+
return sources
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def local_proof_artifact_sources(state):
|
|
299
|
+
return [artifact for artifact in collect_proof_artifact_sources(state) if artifact.get('path') and os.path.exists(artifact.get('path'))]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def local_artifact_sources_fingerprint(local_sources):
|
|
303
|
+
digest = hashlib.sha256()
|
|
304
|
+
for artifact in sorted(local_sources, key=lambda item: item.get('path') or ''):
|
|
305
|
+
path_value = artifact.get('path') or ''
|
|
306
|
+
if not path_value or not os.path.exists(path_value):
|
|
307
|
+
continue
|
|
308
|
+
digest.update(path_value.encode('utf-8'))
|
|
309
|
+
digest.update(str(artifact.get('name') or '').encode('utf-8'))
|
|
310
|
+
digest.update(str(os.path.getsize(path_value)).encode('utf-8'))
|
|
311
|
+
with open(path_value, 'rb') as f:
|
|
312
|
+
while True:
|
|
313
|
+
chunk = f.read(1024 * 1024)
|
|
314
|
+
if not chunk:
|
|
315
|
+
break
|
|
316
|
+
digest.update(chunk)
|
|
317
|
+
return digest.hexdigest()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def public_proof_artifacts(state):
|
|
321
|
+
published = state.get('proof_artifact_publication') or {}
|
|
322
|
+
artifacts = published.get('artifacts') if isinstance(published, dict) else []
|
|
323
|
+
public = [artifact for artifact in artifacts or [] if isinstance(artifact, dict) and (artifact.get('raw_url') or artifact.get('html_url'))]
|
|
324
|
+
for artifact in collect_proof_artifact_sources(state):
|
|
325
|
+
url = str(artifact.get('url') or '').strip()
|
|
326
|
+
if is_http_url(url):
|
|
327
|
+
public.append({
|
|
328
|
+
**artifact,
|
|
329
|
+
'raw_url': url,
|
|
330
|
+
'html_url': url,
|
|
331
|
+
'published': False,
|
|
332
|
+
})
|
|
333
|
+
deduped = []
|
|
334
|
+
seen = set()
|
|
335
|
+
for artifact in public:
|
|
336
|
+
key = artifact.get('raw_url') or artifact.get('html_url') or artifact.get('url')
|
|
337
|
+
if key and key not in seen:
|
|
338
|
+
seen.add(key)
|
|
339
|
+
deduped.append(artifact)
|
|
340
|
+
return deduped
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def first_public_artifact_url(state, role, kind=None):
|
|
344
|
+
for artifact in public_proof_artifacts(state):
|
|
345
|
+
if artifact.get('role') != role:
|
|
346
|
+
continue
|
|
347
|
+
if kind and artifact.get('kind') != kind:
|
|
348
|
+
continue
|
|
349
|
+
return artifact.get('raw_url') or artifact.get('html_url') or artifact.get('url') or ''
|
|
350
|
+
return ''
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def resolve_github_repo_name(repo_dir):
|
|
354
|
+
result = sp.run(['gh', 'repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'],
|
|
355
|
+
cwd=repo_dir, capture_output=True, text=True, timeout=30)
|
|
356
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
357
|
+
return result.stdout.strip()
|
|
358
|
+
remote = git_stdout(['config', '--get', 'remote.origin.url'], repo_dir)
|
|
359
|
+
match = re.search(r'github\.com[:/]([^/]+)/([^/.]+)(?:\.git)?$', remote)
|
|
360
|
+
if match:
|
|
361
|
+
return match.group(1) + '/' + match.group(2)
|
|
362
|
+
return ''
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def write_artifact_readme(path_value, state, artifacts):
|
|
366
|
+
lines = [
|
|
367
|
+
'# Riddle Proof Artifacts',
|
|
368
|
+
'',
|
|
369
|
+
'Run id: `' + str(state.get('run_id') or '') + '`',
|
|
370
|
+
'PR: ' + str(state.get('pr_url') or ''),
|
|
371
|
+
'Goal: ' + str(state.get('change_request') or ''),
|
|
372
|
+
'Verification mode: ' + str(state.get('verification_mode') or 'proof'),
|
|
373
|
+
'',
|
|
374
|
+
'## Artifacts',
|
|
375
|
+
'',
|
|
376
|
+
]
|
|
377
|
+
for artifact in artifacts:
|
|
378
|
+
rel = artifact.get('filename') or artifact.get('published_path') or ''
|
|
379
|
+
label = artifact.get('role', 'artifact') + ' / ' + artifact.get('name', rel)
|
|
380
|
+
if artifact.get('kind') == 'image':
|
|
381
|
+
lines.append('- ' + label + ': `'+ rel + '`')
|
|
382
|
+
lines.append('')
|
|
383
|
+
lines.append(' ')
|
|
384
|
+
lines.append('')
|
|
385
|
+
else:
|
|
386
|
+
lines.append('- [' + label + '](' + rel + ')')
|
|
387
|
+
with open(path_value, 'w') as f:
|
|
388
|
+
f.write('\n'.join(lines).rstrip() + '\n')
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def publish_local_proof_artifacts_to_github(state, repo_dir, pr_num):
|
|
392
|
+
local_sources = local_proof_artifact_sources(state)
|
|
393
|
+
if not local_sources:
|
|
394
|
+
state['proof_artifact_publication'] = {'ok': True, 'skipped': True, 'reason': 'no local file artifacts'}
|
|
395
|
+
save_state(state)
|
|
396
|
+
return state['proof_artifact_publication']
|
|
397
|
+
|
|
398
|
+
source_fingerprint = local_artifact_sources_fingerprint(local_sources)
|
|
399
|
+
existing = state.get('proof_artifact_publication') or {}
|
|
400
|
+
if existing.get('ok') and existing.get('artifacts') and existing.get('source_fingerprint') == source_fingerprint:
|
|
401
|
+
return existing
|
|
402
|
+
|
|
403
|
+
repo_name = resolve_github_repo_name(repo_dir)
|
|
404
|
+
if not repo_name:
|
|
405
|
+
raise SystemExit('Could not resolve GitHub repository name for proof artifact publication.')
|
|
406
|
+
|
|
407
|
+
run_id = safe_slug(state.get('run_id') or str(int(time.time())), 'run')
|
|
408
|
+
pr_slug = safe_slug('pr-' + str(pr_num or 'unknown'), 'pr')
|
|
409
|
+
artifact_branch = safe_slug('riddle-proof-artifacts-' + pr_slug + '-' + run_id, 'riddle-proof-artifacts')
|
|
410
|
+
artifact_dir_name = 'riddle-proof/' + run_id
|
|
411
|
+
tmp = tempfile.mkdtemp(prefix='riddle-proof-gh-artifacts-')
|
|
412
|
+
published = []
|
|
413
|
+
try:
|
|
414
|
+
git_checked(['init'], tmp)
|
|
415
|
+
origin = git_stdout(['config', '--get', 'remote.origin.url'], repo_dir)
|
|
416
|
+
git_checked(['remote', 'add', 'origin', origin], tmp)
|
|
417
|
+
git_checked(['checkout', '-b', artifact_branch], tmp)
|
|
418
|
+
git_checked(['config', 'user.name', 'Riddle Proof'], tmp)
|
|
419
|
+
git_checked(['config', 'user.email', 'riddle-proof@riddledc.com'], tmp)
|
|
420
|
+
target_dir = os.path.join(tmp, artifact_dir_name)
|
|
421
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
422
|
+
used_names = set()
|
|
423
|
+
for artifact in local_sources:
|
|
424
|
+
source_path = artifact.get('path')
|
|
425
|
+
try:
|
|
426
|
+
size = os.path.getsize(source_path)
|
|
427
|
+
except Exception:
|
|
428
|
+
continue
|
|
429
|
+
if size > MAX_GITHUB_PROOF_ARTIFACT_BYTES:
|
|
430
|
+
published.append({**artifact, 'published': False, 'skipped': True, 'reason': 'artifact exceeds size limit'})
|
|
431
|
+
continue
|
|
432
|
+
base_name = artifact_name(artifact.get('name'), source_path, artifact.get('role', 'artifact') + '-artifact')
|
|
433
|
+
filename = base_name
|
|
434
|
+
stem, ext = os.path.splitext(base_name)
|
|
435
|
+
counter = 2
|
|
436
|
+
while filename in used_names:
|
|
437
|
+
filename = stem + '-' + str(counter) + ext
|
|
438
|
+
counter += 1
|
|
439
|
+
used_names.add(filename)
|
|
440
|
+
dest = os.path.join(target_dir, filename)
|
|
441
|
+
shutil.copyfile(source_path, dest)
|
|
442
|
+
published_path = artifact_dir_name + '/' + filename
|
|
443
|
+
digest = hashlib.sha256(open(source_path, 'rb').read()).hexdigest()
|
|
444
|
+
published.append({
|
|
445
|
+
**artifact,
|
|
446
|
+
'filename': filename,
|
|
447
|
+
'published_path': published_path,
|
|
448
|
+
'size_bytes': size,
|
|
449
|
+
'sha256': digest,
|
|
450
|
+
'published': True,
|
|
451
|
+
})
|
|
452
|
+
write_artifact_readme(os.path.join(target_dir, 'README.md'), state, [a for a in published if a.get('published')])
|
|
453
|
+
with open(os.path.join(target_dir, 'proof-artifacts.json'), 'w') as f:
|
|
454
|
+
json.dump({
|
|
455
|
+
'version': 'riddle-proof.github-artifacts.v1',
|
|
456
|
+
'run_id': state.get('run_id', ''),
|
|
457
|
+
'pr_url': state.get('pr_url', ''),
|
|
458
|
+
'artifacts': published,
|
|
459
|
+
}, f, indent=2)
|
|
460
|
+
git_checked(['add', '.'], tmp)
|
|
461
|
+
git_checked(['commit', '-m', 'Publish Riddle Proof artifacts'], tmp)
|
|
462
|
+
commit = git_stdout(['rev-parse', 'HEAD'], tmp)
|
|
463
|
+
push = sp.run(['git', 'push', 'origin', 'HEAD:refs/heads/' + artifact_branch, '--force'],
|
|
464
|
+
cwd=tmp, capture_output=True, text=True, timeout=120)
|
|
465
|
+
if push.returncode != 0:
|
|
466
|
+
raise SystemExit('Failed to push proof artifact branch: ' + push.stderr[:300])
|
|
467
|
+
for artifact in published:
|
|
468
|
+
if artifact.get('published'):
|
|
469
|
+
published_path = artifact.get('published_path')
|
|
470
|
+
artifact['raw_url'] = 'https://raw.githubusercontent.com/' + repo_name + '/' + commit + '/' + published_path
|
|
471
|
+
artifact['html_url'] = 'https://github.com/' + repo_name + '/blob/' + commit + '/' + published_path
|
|
472
|
+
publication = {
|
|
473
|
+
'ok': True,
|
|
474
|
+
'branch': artifact_branch,
|
|
475
|
+
'commit': commit,
|
|
476
|
+
'repo': repo_name,
|
|
477
|
+
'html_url': 'https://github.com/' + repo_name + '/tree/' + commit + '/' + artifact_dir_name,
|
|
478
|
+
'manifest_url': 'https://github.com/' + repo_name + '/blob/' + commit + '/' + artifact_dir_name + '/proof-artifacts.json',
|
|
479
|
+
'readme_url': 'https://github.com/' + repo_name + '/blob/' + commit + '/' + artifact_dir_name + '/README.md',
|
|
480
|
+
'source_fingerprint': source_fingerprint,
|
|
481
|
+
'artifacts': published,
|
|
482
|
+
}
|
|
483
|
+
state['proof_artifact_publication'] = publication
|
|
484
|
+
save_state(state)
|
|
485
|
+
return publication
|
|
486
|
+
finally:
|
|
487
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
488
|
+
|
|
489
|
+
|
|
188
490
|
def is_temp_proof_branch(branch):
|
|
189
491
|
return str(branch or '').strip().startswith('riddle-proof/')
|
|
190
492
|
|
|
@@ -271,6 +573,10 @@ def build_ship_report(state, marked_ready=None):
|
|
|
271
573
|
branch = state.get('target_branch') or state.get('branch') or ''
|
|
272
574
|
if marked_ready is None:
|
|
273
575
|
marked_ready = state.get('marked_ready')
|
|
576
|
+
before_artifact_url = first_public_artifact_url(state, 'before', 'image') or state.get('before_cdn', '')
|
|
577
|
+
prod_artifact_url = first_public_artifact_url(state, 'prod', 'image') or state.get('prod_cdn', '')
|
|
578
|
+
after_artifact_url = first_public_artifact_url(state, 'after', 'image') or state.get('after_cdn', '')
|
|
579
|
+
artifact_publication = state.get('proof_artifact_publication') or {}
|
|
274
580
|
return {
|
|
275
581
|
'pr_url': state.get('pr_url', ''),
|
|
276
582
|
'pr_branch': branch,
|
|
@@ -282,9 +588,12 @@ def build_ship_report(state, marked_ready=None):
|
|
|
282
588
|
'ci_status': state.get('ci_status', ''),
|
|
283
589
|
'proof_comment_url': state.get('proof_comment_url', ''),
|
|
284
590
|
'proof_assessment_comment_url': state.get('proof_assessment_comment_url', ''),
|
|
285
|
-
'before_artifact_url':
|
|
286
|
-
'prod_artifact_url':
|
|
287
|
-
'after_artifact_url':
|
|
591
|
+
'before_artifact_url': before_artifact_url,
|
|
592
|
+
'prod_artifact_url': prod_artifact_url,
|
|
593
|
+
'after_artifact_url': after_artifact_url,
|
|
594
|
+
'proof_artifacts_url': artifact_publication.get('html_url', '') if isinstance(artifact_publication, dict) else '',
|
|
595
|
+
'proof_artifacts_manifest_url': artifact_publication.get('manifest_url', '') if isinstance(artifact_publication, dict) else '',
|
|
596
|
+
'proof_artifact_publication': artifact_publication if isinstance(artifact_publication, dict) else {},
|
|
288
597
|
}
|
|
289
598
|
|
|
290
599
|
|
|
@@ -515,12 +824,15 @@ def post_discord_ready_message(state, marked_ready):
|
|
|
515
824
|
else:
|
|
516
825
|
ci_line = 'CI was not confirmed green yet; review the PR checks before merging.'
|
|
517
826
|
proof_bits = []
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
827
|
+
before_url = first_public_artifact_url(state, 'before', 'image') or state.get('before_cdn')
|
|
828
|
+
prod_url = first_public_artifact_url(state, 'prod', 'image') or state.get('prod_cdn')
|
|
829
|
+
after_url = first_public_artifact_url(state, 'after', 'image') or state.get('after_cdn')
|
|
830
|
+
if before_url:
|
|
831
|
+
proof_bits.append('before: ' + before_url)
|
|
832
|
+
if prod_url:
|
|
833
|
+
proof_bits.append('prod: ' + prod_url)
|
|
834
|
+
if after_url:
|
|
835
|
+
proof_bits.append('after: ' + after_url)
|
|
524
836
|
elif state_has_after_evidence(state):
|
|
525
837
|
proof_bits.append('after: structured evidence bundle')
|
|
526
838
|
|
|
@@ -671,6 +983,8 @@ if s.get('finalized') and s.get('pr_url') and existing_after_dir and not os.path
|
|
|
671
983
|
'proof_comment_url': report.get('proof_comment_url', ''),
|
|
672
984
|
'before_artifact_url': report.get('before_artifact_url', ''),
|
|
673
985
|
'after_artifact_url': report.get('after_artifact_url', ''),
|
|
986
|
+
'proof_artifacts_url': report.get('proof_artifacts_url', ''),
|
|
987
|
+
'proof_artifacts_manifest_url': report.get('proof_artifacts_manifest_url', ''),
|
|
674
988
|
'finalized_retry': True,
|
|
675
989
|
'proof_assessment_comment_posted': bool(s.get('proof_assessment_comment_posted')),
|
|
676
990
|
'discord_notification': s.get('discord_notification'),
|
|
@@ -755,6 +1069,12 @@ pr_num = s.get('pr_number', '')
|
|
|
755
1069
|
if not pr_num:
|
|
756
1070
|
raise SystemExit('No PR created. Check gh auth.')
|
|
757
1071
|
|
|
1072
|
+
publication = publish_local_proof_artifacts_to_github(s, repo_dir, pr_num)
|
|
1073
|
+
if publication.get('ok') and not publication.get('skipped'):
|
|
1074
|
+
print('Proof artifacts published: ' + publication.get('html_url', ''))
|
|
1075
|
+
elif publication.get('skipped'):
|
|
1076
|
+
print('Proof artifact publication skipped: ' + publication.get('reason', 'unknown'))
|
|
1077
|
+
|
|
758
1078
|
# Post proof comment on PR
|
|
759
1079
|
body = '## Riddle Proof — Proof of Fix\n\n'
|
|
760
1080
|
body += '**Goal:** ' + s.get('change_request', '') + '\n\n'
|
|
@@ -762,14 +1082,39 @@ if s.get('success_criteria'):
|
|
|
762
1082
|
body += '**Success criteria:** ' + s['success_criteria'] + '\n\n'
|
|
763
1083
|
body += '**Verification mode:** ' + s.get('verification_mode', 'proof') + '\n\n'
|
|
764
1084
|
body += '**Merge recommendation:** ' + effective_merge_recommendation(s) + '\n\n'
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if
|
|
768
|
-
body += '
|
|
769
|
-
|
|
770
|
-
|
|
1085
|
+
|
|
1086
|
+
public_artifacts = public_proof_artifacts(s)
|
|
1087
|
+
if publication.get('ok') and not publication.get('skipped'):
|
|
1088
|
+
body += '**Proof artifacts:** '
|
|
1089
|
+
body += '[bundle](' + publication.get('html_url', '') + ')'
|
|
1090
|
+
if publication.get('manifest_url'):
|
|
1091
|
+
body += ' | [manifest](' + publication.get('manifest_url', '') + ')'
|
|
1092
|
+
body += '\n\n'
|
|
1093
|
+
|
|
1094
|
+
before_image = first_public_artifact_url(s, 'before', 'image')
|
|
1095
|
+
prod_image = first_public_artifact_url(s, 'prod', 'image')
|
|
1096
|
+
after_image = first_public_artifact_url(s, 'after', 'image')
|
|
1097
|
+
if before_image:
|
|
1098
|
+
body += '### Before\n\n\n'
|
|
1099
|
+
if prod_image:
|
|
1100
|
+
body += '### Prod\n\n\n'
|
|
1101
|
+
if after_image:
|
|
1102
|
+
body += '### After\n\n\n'
|
|
771
1103
|
else:
|
|
772
1104
|
body += '### After evidence\nNo after screenshot was captured for this verification mode; structured evidence is summarized below.\n\n'
|
|
1105
|
+
|
|
1106
|
+
data_artifacts = [
|
|
1107
|
+
artifact for artifact in public_artifacts
|
|
1108
|
+
if artifact.get('kind') != 'image' and (artifact.get('html_url') or artifact.get('raw_url'))
|
|
1109
|
+
]
|
|
1110
|
+
if data_artifacts:
|
|
1111
|
+
body += '### Structured artifacts\n'
|
|
1112
|
+
for artifact in data_artifacts[:12]:
|
|
1113
|
+
label = artifact.get('name') or artifact.get('filename') or artifact.get('kind') or 'artifact'
|
|
1114
|
+
url = artifact.get('html_url') or artifact.get('raw_url')
|
|
1115
|
+
body += '- [' + str(label) + '](' + str(url) + ')\n'
|
|
1116
|
+
body += '\n'
|
|
1117
|
+
|
|
773
1118
|
bundle_text = evidence_bundle_text(s)
|
|
774
1119
|
if bundle_text:
|
|
775
1120
|
body += '### Evidence bundle\n```\n' + bundle_text + '\n```\n\n'
|
|
@@ -904,5 +1249,7 @@ print(json.dumps({
|
|
|
904
1249
|
'before_artifact_url': report.get('before_artifact_url', ''),
|
|
905
1250
|
'prod_artifact_url': report.get('prod_artifact_url', ''),
|
|
906
1251
|
'after_artifact_url': report.get('after_artifact_url', ''),
|
|
1252
|
+
'proof_artifacts_url': report.get('proof_artifacts_url', ''),
|
|
1253
|
+
'proof_artifacts_manifest_url': report.get('proof_artifacts_manifest_url', ''),
|
|
907
1254
|
'ship_report': report,
|
|
908
1255
|
}))
|
package/runtime/lib/util.py
CHANGED
|
@@ -621,37 +621,65 @@ def nested_non_riddle_enabled():
|
|
|
621
621
|
def invoke_riddle_core(tool, args, timeout=180):
|
|
622
622
|
"""Call Riddle's shared core package directly, without nested OpenClaw tool invocation."""
|
|
623
623
|
script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'riddle_core_call.mjs')
|
|
624
|
+
result_file = tempfile.NamedTemporaryFile(prefix='riddle-proof-direct-', suffix='.json', delete=False).name
|
|
625
|
+
stderr_file = tempfile.NamedTemporaryFile(prefix='riddle-proof-direct-', suffix='.stderr', delete=False).name
|
|
626
|
+
env = dict(os.environ)
|
|
627
|
+
env['RIDDLE_PROOF_DIRECT_RESULT_FILE'] = result_file
|
|
628
|
+
stderr_text = ''
|
|
624
629
|
try:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
630
|
+
with open(stderr_file, 'w', encoding='utf-8') as stderr_handle:
|
|
631
|
+
r = sp.run(
|
|
632
|
+
['node', script, tool, json.dumps(args)],
|
|
633
|
+
stdout=sp.DEVNULL, stderr=stderr_handle, text=True, timeout=timeout, env=env
|
|
634
|
+
)
|
|
629
635
|
except sp.TimeoutExpired as e:
|
|
630
636
|
print('direct_riddle(' + tool + ') TIMED OUT after ' + str(timeout) + 's')
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
637
|
+
try:
|
|
638
|
+
with open(stderr_file, 'r', encoding='utf-8') as f:
|
|
639
|
+
stderr_text = f.read()[:500]
|
|
640
|
+
except Exception:
|
|
641
|
+
stderr_text = ''
|
|
642
|
+
if stderr_text:
|
|
643
|
+
print(' stderr: ' + stderr_text)
|
|
635
644
|
return {
|
|
636
645
|
'ok': False,
|
|
637
646
|
'timeout': True,
|
|
638
647
|
'error': f'direct_riddle({tool}) timed out after {timeout}s',
|
|
639
|
-
'
|
|
640
|
-
'stderr': (e.stderr or '')[:500],
|
|
648
|
+
'stderr': stderr_text[:500],
|
|
641
649
|
}
|
|
650
|
+
finally:
|
|
651
|
+
if 'r' not in locals():
|
|
652
|
+
for temp_path in (result_file, stderr_file):
|
|
653
|
+
try:
|
|
654
|
+
os.unlink(temp_path)
|
|
655
|
+
except Exception:
|
|
656
|
+
pass
|
|
657
|
+
try:
|
|
658
|
+
with open(stderr_file, 'r', encoding='utf-8') as f:
|
|
659
|
+
stderr_text = f.read()
|
|
660
|
+
except Exception:
|
|
661
|
+
stderr_text = ''
|
|
642
662
|
|
|
643
663
|
if r.returncode != 0:
|
|
644
664
|
print('direct_riddle(' + tool + ') FAILED rc=' + str(r.returncode))
|
|
645
|
-
print('
|
|
646
|
-
print(' stderr: ' + r.stderr[:500])
|
|
665
|
+
print(' stderr: ' + stderr_text[:500])
|
|
647
666
|
|
|
648
667
|
try:
|
|
649
|
-
|
|
650
|
-
|
|
668
|
+
with open(result_file, 'r', encoding='utf-8') as f:
|
|
669
|
+
return json.loads(f.read())
|
|
670
|
+
except Exception:
|
|
651
671
|
print('direct_riddle(' + tool + ') JSON parse failed')
|
|
652
|
-
print('
|
|
653
|
-
|
|
654
|
-
|
|
672
|
+
print(' stderr: ' + stderr_text[:500])
|
|
673
|
+
return {'ok': False, 'error': 'direct_riddle result file missing or invalid', 'stderr': stderr_text[:300]}
|
|
674
|
+
finally:
|
|
675
|
+
try:
|
|
676
|
+
os.unlink(result_file)
|
|
677
|
+
except Exception:
|
|
678
|
+
pass
|
|
679
|
+
try:
|
|
680
|
+
os.unlink(stderr_file)
|
|
681
|
+
except Exception:
|
|
682
|
+
pass
|
|
655
683
|
|
|
656
684
|
|
|
657
685
|
def invoke(tool, args, timeout=180):
|
|
@@ -771,7 +799,12 @@ def invoke_retry(tool, args, retries=3, timeout=180):
|
|
|
771
799
|
result = invoke(tool, args, timeout=timeout)
|
|
772
800
|
last_result = result
|
|
773
801
|
# Check for success indicators
|
|
774
|
-
if result.get('ok')
|
|
802
|
+
if result.get('ok'):
|
|
803
|
+
return result
|
|
804
|
+
if tool == 'riddle_script' and (result.get('error') or result.get('script_error')):
|
|
805
|
+
print('invoke_retry(riddle_script) stopping early for deterministic script error')
|
|
806
|
+
return result
|
|
807
|
+
if result.get('outputs') or result.get('screenshots'):
|
|
775
808
|
return result
|
|
776
809
|
print(f'invoke_retry({tool}) attempt {attempt}/{retries} failed: {str(result.get("error", "no output"))[:200]}')
|
|
777
810
|
if tool == 'riddle_script' and non_retryable_riddle_script_error(result):
|
|
@@ -911,6 +944,39 @@ def summarize_capture_artifact_item(item):
|
|
|
911
944
|
return {key: value for key, value in summary.items() if value not in (None, '')}
|
|
912
945
|
|
|
913
946
|
|
|
947
|
+
def capture_screenshot_url(payload, label=''):
|
|
948
|
+
if not isinstance(payload, dict):
|
|
949
|
+
return ''
|
|
950
|
+
enriched = enrich_capture_payload(payload)
|
|
951
|
+
candidates = []
|
|
952
|
+
for key in ('screenshots', 'outputs', 'artifacts'):
|
|
953
|
+
values = enriched.get(key) or []
|
|
954
|
+
if isinstance(values, list):
|
|
955
|
+
candidates.extend([item for item in values if isinstance(item, dict)])
|
|
956
|
+
|
|
957
|
+
requested = (label or '').strip()
|
|
958
|
+
expected_names = set()
|
|
959
|
+
if requested:
|
|
960
|
+
expected_names.update({
|
|
961
|
+
requested,
|
|
962
|
+
requested + '.png',
|
|
963
|
+
requested + '.jpg',
|
|
964
|
+
requested + '.jpeg',
|
|
965
|
+
requested + '.webp',
|
|
966
|
+
})
|
|
967
|
+
for item in candidates:
|
|
968
|
+
name = str(item.get('name') or '')
|
|
969
|
+
url = str(item.get('url') or '')
|
|
970
|
+
if url and expected_names and name in expected_names:
|
|
971
|
+
return url
|
|
972
|
+
for item in candidates:
|
|
973
|
+
url = str(item.get('url') or '')
|
|
974
|
+
name = str(item.get('name') or '')
|
|
975
|
+
if url and (not name or re.search(r'\.(png|jpe?g|webp|gif)$', name, re.I)):
|
|
976
|
+
return url
|
|
977
|
+
return ''
|
|
978
|
+
|
|
979
|
+
|
|
914
980
|
def git(cmd, cwd):
|
|
915
981
|
"""Run a shell command in a repo directory."""
|
|
916
982
|
return sp.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
|
|
@@ -1152,7 +1218,7 @@ def build_capture_script(url, capture_script, label, wait_for_selector='', viewp
|
|
|
1152
1218
|
effective_viewport_matrix = None if script_handles_viewport_matrix else viewport_matrix
|
|
1153
1219
|
pieces = [
|
|
1154
1220
|
*viewport_matrix_setup_js(effective_viewport_matrix),
|
|
1155
|
-
'await page.goto(' + json.dumps(url) + ');',
|
|
1221
|
+
'await page.goto(' + json.dumps(url) + ', { waitUntil: "domcontentloaded", timeout: 30000 });',
|
|
1156
1222
|
]
|
|
1157
1223
|
selector = (wait_for_selector or '').strip()
|
|
1158
1224
|
if selector:
|
|
@@ -1179,33 +1245,44 @@ def capture_static_preview(state, project_dir, label, capture_script, timeout=30
|
|
|
1179
1245
|
'raw': {'ok': False, 'error': 'No static build output found. Tried configured build_output, dist, build, out.'},
|
|
1180
1246
|
}
|
|
1181
1247
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1248
|
+
static_server_command = (
|
|
1249
|
+
(state.get('static_server_command') or '').strip()
|
|
1250
|
+
or os.environ.get('RIDDLE_PROOF_STATIC_SERVER_COMMAND', '').strip()
|
|
1251
|
+
or 'python3 -m http.server "$PORT" --bind 127.0.0.1'
|
|
1252
|
+
)
|
|
1253
|
+
target = target_path or state.get('server_path', '') or '/'
|
|
1254
|
+
args = {
|
|
1255
|
+
'directory': build_dir,
|
|
1256
|
+
'command': static_server_command,
|
|
1257
|
+
'port': int(state.get('server_port') or 3000),
|
|
1258
|
+
'wait_until': 'domcontentloaded',
|
|
1259
|
+
'readiness_timeout': 60,
|
|
1260
|
+
'timeout': max(60, min(int(timeout or 300), 300)),
|
|
1261
|
+
'path': target,
|
|
1262
|
+
'readiness_path': '/',
|
|
1263
|
+
'script': capture_script,
|
|
1264
|
+
}
|
|
1265
|
+
if state.get('wait_for_selector'):
|
|
1266
|
+
args['wait_for_selector'] = state.get('wait_for_selector')
|
|
1267
|
+
if state.get('color_scheme'):
|
|
1268
|
+
args['color_scheme'] = state.get('color_scheme')
|
|
1197
1269
|
apply_auth_context(state, args)
|
|
1198
|
-
shot = invoke_retry('
|
|
1270
|
+
shot = invoke_retry('riddle_server_preview', args, retries=2, timeout=max(timeout, 120))
|
|
1199
1271
|
screenshots = shot.get('screenshots') or []
|
|
1200
|
-
url = screenshots[0].get('url', '') if screenshots else
|
|
1272
|
+
url = screenshots[0].get('url', '') if screenshots else capture_screenshot_url(shot, label)
|
|
1201
1273
|
return {
|
|
1202
1274
|
'ok': bool(url),
|
|
1203
|
-
'preview_id':
|
|
1204
|
-
'preview_url': preview_url,
|
|
1205
|
-
'capture_url':
|
|
1275
|
+
'preview_id': '',
|
|
1276
|
+
'preview_url': shot.get('preview_url') or '',
|
|
1277
|
+
'capture_url': shot.get('target_url') or target,
|
|
1206
1278
|
'url': url,
|
|
1207
1279
|
'raw': {
|
|
1208
|
-
'preview':
|
|
1280
|
+
'preview': {
|
|
1281
|
+
'ok': shot.get('ok'),
|
|
1282
|
+
'runner': shot.get('runner'),
|
|
1283
|
+
'preview_url': shot.get('preview_url') or '',
|
|
1284
|
+
'target_url': shot.get('target_url') or '',
|
|
1285
|
+
},
|
|
1209
1286
|
'capture': shot,
|
|
1210
1287
|
},
|
|
1211
1288
|
}
|