@riddledc/riddle-proof 0.5.0 → 0.5.2

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.
Files changed (35) hide show
  1. package/README.md +6 -6
  2. package/dist/chunk-3MHFLQKG.js +853 -0
  3. package/dist/{chunk-LVP22WE4.js → chunk-5GZZZ6JA.js} +5 -1
  4. package/dist/engine-harness.cjs +2505 -22
  5. package/dist/engine-harness.js +1 -1
  6. package/dist/index.cjs +2512 -29
  7. package/dist/index.js +2 -2
  8. package/dist/openclaw.cjs +1 -1
  9. package/dist/openclaw.js +1 -1
  10. package/dist/proof-run-core.cjs +909 -0
  11. package/dist/proof-run-core.d.cts +280 -0
  12. package/dist/proof-run-core.d.ts +280 -0
  13. package/dist/proof-run-core.js +48 -0
  14. package/dist/proof-run-engine.cjs +2499 -0
  15. package/dist/proof-run-engine.d.cts +677 -0
  16. package/dist/proof-run-engine.d.ts +677 -0
  17. package/dist/proof-run-engine.js +1649 -0
  18. package/lib/workspace-core.mjs +391 -0
  19. package/package.json +15 -3
  20. package/runtime/lib/author.py +343 -0
  21. package/runtime/lib/implement.py +63 -0
  22. package/runtime/lib/preflight.py +246 -0
  23. package/runtime/lib/recon.py +1048 -0
  24. package/runtime/lib/riddle_core_call.mjs +151 -0
  25. package/runtime/lib/setup.py +387 -0
  26. package/runtime/lib/ship.py +834 -0
  27. package/runtime/lib/util.py +673 -0
  28. package/runtime/lib/verify.py +1223 -0
  29. package/runtime/pipelines/riddle-proof-author.lobster +28 -0
  30. package/runtime/pipelines/riddle-proof-implement.lobster +26 -0
  31. package/runtime/pipelines/riddle-proof-recon.lobster +79 -0
  32. package/runtime/pipelines/riddle-proof-setup.lobster +141 -0
  33. package/runtime/pipelines/riddle-proof-ship.lobster +36 -0
  34. package/runtime/pipelines/riddle-proof-verify.lobster +74 -0
  35. package/runtime/tests/recon_verify_smoke.py +1198 -0
@@ -0,0 +1,343 @@
1
+ """Author: prepare or apply the supervising agent's proof packet.
2
+
3
+ This stage no longer delegates proof authoring to an embedded alternate model.
4
+ Instead it does two things:
5
+ - distill recon state into a structured request for the supervising agent
6
+ - normalize and persist a supervisor-supplied proof packet for later stages
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+ from util import load_state, save_state
15
+
16
+
17
+ RUNTIME_MODEL_HINT = (
18
+ os.environ.get('RIDDLE_PROOF_AUTHOR_RUNTIME_MODEL', '').strip()
19
+ or os.environ.get('OPENCLAW_MODEL', '').strip()
20
+ or os.environ.get('OPENCLAW_DEFAULT_MODEL', '').strip()
21
+ or os.environ.get('OPENCLAW_RUNTIME_MODEL', '').strip()
22
+ or os.environ.get('AGENT_MODEL', '').strip()
23
+ or os.environ.get('DEFAULT_MODEL', '').strip()
24
+ or os.environ.get('MODEL', '').strip()
25
+ )
26
+
27
+
28
+ def normalize_path(value):
29
+ path = (value or '').strip()
30
+ if not path:
31
+ return ''
32
+ if not path.startswith('/'):
33
+ path = '/' + path
34
+ return path
35
+
36
+
37
+ def first_non_empty(*values):
38
+ for value in values:
39
+ if isinstance(value, str) and value.strip():
40
+ return value.strip()
41
+ return ''
42
+
43
+
44
+ def sanitize_rationale(value):
45
+ if isinstance(value, str):
46
+ value = [value]
47
+ if not isinstance(value, list):
48
+ return []
49
+ out = []
50
+ for item in value:
51
+ if isinstance(item, str) and item.strip():
52
+ out.append(item.strip())
53
+ return out[:6]
54
+
55
+
56
+ def recon_baseline_understanding(state):
57
+ assessment = state.get('recon_assessment') or {}
58
+ understanding = assessment.get('baseline_understanding') or state.get('recon_baseline_understanding') or {}
59
+ return understanding if isinstance(understanding, dict) else {}
60
+
61
+
62
+ def authored_capture_script(existing_script, wait_for_selector=''):
63
+ script = (existing_script or '').strip()
64
+ if script:
65
+ return script
66
+ steps = ['await page.waitForTimeout(1500);']
67
+ selector = (wait_for_selector or '').strip()
68
+ if selector:
69
+ steps.append('await page.waitForSelector(' + json.dumps(selector) + ');')
70
+ steps.append("await saveScreenshot('after-proof');")
71
+ return ' '.join(steps)
72
+
73
+
74
+ def authored_proof_plan(state, reference, target_path, baselines, wait_for_selector=''):
75
+ existing = (state.get('proof_plan') or '').strip()
76
+ if existing:
77
+ return existing
78
+
79
+ lines = []
80
+ change_request = (state.get('change_request') or '').strip()
81
+ success_criteria = (state.get('success_criteria') or '').strip()
82
+ verification_mode = (state.get('verification_mode') or 'proof').strip() or 'proof'
83
+
84
+ if change_request:
85
+ lines.append('Goal: ' + change_request)
86
+ lines.append('Verification mode: ' + verification_mode)
87
+ if success_criteria:
88
+ lines.append('Success criteria: ' + success_criteria)
89
+ lines.append('Target route: ' + (target_path or '/'))
90
+ lines.append('Reference baseline: ' + reference)
91
+ if baselines.get('before', {}).get('url'):
92
+ lines.append('Reuse recon before baseline: ' + baselines['before']['url'])
93
+ if baselines.get('prod', {}).get('url'):
94
+ lines.append('Reuse recon prod baseline: ' + baselines['prod']['url'])
95
+ baseline_understanding = recon_baseline_understanding(state)
96
+ visible_before = (baseline_understanding.get('visible_before_state') or '').strip()
97
+ proof_focus = (baseline_understanding.get('proof_focus') or '').strip()
98
+ stop_condition = (baseline_understanding.get('stop_condition') or '').strip()
99
+ if visible_before:
100
+ lines.append('Observed before state: ' + visible_before)
101
+ if proof_focus:
102
+ lines.append('Proof focus: ' + proof_focus)
103
+ if stop_condition:
104
+ lines.append('Stop condition: ' + stop_condition)
105
+ if wait_for_selector:
106
+ lines.append('Stabilize capture on selector: ' + wait_for_selector)
107
+ lines.append('After evidence should load the recon-confirmed route and collect the evidence type required by verification_mode without rediscovering baseline context.')
108
+ lines.append('For visual modes this usually means a stable screenshot; for data, audio, log, metric, or custom modes it may mean structured proofEvidence, JSON artifacts, console observations, or assertions.')
109
+ lines.append('Revise this draft only when the supervising agent concludes the proof needs richer interactions, better sense data, or tighter framing.')
110
+ return '\n'.join(lines)
111
+
112
+
113
+ def author_request_payload(state, reference, baselines, current_plan, hypothesis, fallback_path, fallback_selector):
114
+ recon_results = state.get('recon_results') or {}
115
+ attempt_history = recon_results.get('attempt_history') or []
116
+ trimmed_attempts = []
117
+ for item in attempt_history[-3:]:
118
+ trimmed_attempts.append({
119
+ 'attempt': item.get('attempt'),
120
+ 'result': item.get('result'),
121
+ 'plan': item.get('plan'),
122
+ 'observations': item.get('observations'),
123
+ })
124
+
125
+ fallback_capture_script = authored_capture_script(state.get('capture_script'), fallback_selector)
126
+ fallback_proof_plan = authored_proof_plan(state, reference, fallback_path, baselines, fallback_selector)
127
+
128
+ return {
129
+ 'status': 'needs_supervisor_judgment',
130
+ 'goal': (state.get('change_request') or '').strip(),
131
+ 'success_criteria': (state.get('success_criteria') or '').strip(),
132
+ 'verification_mode': (state.get('verification_mode') or 'proof').strip() or 'proof',
133
+ 'reference': reference,
134
+ 'baseline_understanding': recon_baseline_understanding(state),
135
+ 'observed_baselines': baselines,
136
+ 'current_plan': current_plan or {},
137
+ 'hypothesis': hypothesis or {},
138
+ 'route_hints': (state.get('author_request') or {}).get('route_hints') or recon_results.get('route_hints') or [],
139
+ 'keyword_hits': (state.get('author_request') or {}).get('keyword_hits') or recon_results.get('keyword_hits') or [],
140
+ 'recon_summary': (state.get('recon_summary') or '').strip(),
141
+ 'attempt_history_tail': trimmed_attempts,
142
+ 'fallback_defaults': {
143
+ 'server_path': fallback_path,
144
+ 'wait_for_selector': fallback_selector,
145
+ 'capture_script': fallback_capture_script,
146
+ 'proof_plan': fallback_proof_plan,
147
+ },
148
+ 'instructions': [
149
+ 'The supervising agent owns proof authoring. Use the recon-confirmed route and baselines instead of inventing a new context.',
150
+ 'Treat baseline_understanding as the required before-state review. The proof plan must name the observed before state, requested delta, and stop condition.',
151
+ 'Return the authored packet via author_packet_json when possible. You may also set proof_plan, capture_script, server_path, and wait_for_selector directly.',
152
+ 'Keep capture_script concise Playwright statements.',
153
+ 'For visual/UI proof, include saveScreenshot(\'after-proof\') exactly once.',
154
+ 'For data/audio/log/metric/custom proof, screenshots are optional; set window.__riddleProofEvidence inside page.evaluate to a JSON-serializable object with the measured observations the verifier should judge.',
155
+ 'Do not assign globalThis.__riddleProofEvidence, window.__riddleProofEvidence, or self.__riddleProofEvidence outside page.evaluate; the Riddle worker context may not expose those globals safely.',
156
+ 'Do not begin capture_script with page.goto unless an in-app navigation is genuinely required after the preview opens the target route.',
157
+ 'Only escalate to the human after the supervising agent concludes the workflow is genuinely stuck or not converging.',
158
+ ],
159
+ 'response_schema': {
160
+ 'proof_plan': 'string',
161
+ 'capture_script': 'string',
162
+ 'baseline_understanding_used': {
163
+ 'reference': 'before | prod | both | unknown',
164
+ 'target_route': 'string',
165
+ 'before_evidence_url': 'string',
166
+ 'visible_before_state': 'string',
167
+ 'relevant_elements': ['string'],
168
+ 'requested_change': 'string',
169
+ 'proof_focus': 'string',
170
+ 'stop_condition': 'string',
171
+ 'quality_risks': ['string'],
172
+ },
173
+ 'refined_inputs': {
174
+ 'server_path': 'string',
175
+ 'wait_for_selector': 'string',
176
+ 'reference': 'string',
177
+ },
178
+ 'rationale': ['string'],
179
+ 'confidence': 'high | medium | low',
180
+ 'summary': 'string',
181
+ },
182
+ }
183
+
184
+
185
+ s = load_state()
186
+ if s.get('recon_status') not in ('ready_for_proof_plan', 'completed'):
187
+ raise SystemExit('Recon is not ready for proof authoring. Run recon until it produces a usable observation packet first.')
188
+
189
+ recon_results = s.get('recon_results') or {}
190
+ author_request = s.get('author_request') or s.get('proof_plan_request') or {}
191
+ baselines = (author_request.get('observed_baselines') or recon_results.get('baselines') or {})
192
+ current_plan = author_request.get('current_plan') or recon_results.get('current_plan') or {}
193
+ hypothesis = author_request.get('hypothesis') or s.get('recon_hypothesis') or {}
194
+ reference = s.get('requested_reference') or s.get('reference') or author_request.get('reference') or 'before'
195
+
196
+ before_path = ((baselines.get('before') or {}).get('path') or '').strip()
197
+ prod_path = ((baselines.get('prod') or {}).get('path') or '').strip()
198
+ current_path = (current_plan.get('target_path') or '').strip()
199
+ hypothesis_path = (hypothesis.get('target_path') or '').strip()
200
+ existing_path = (s.get('server_path') or '').strip()
201
+ default_path = normalize_path(first_non_empty(before_path, prod_path, current_path, hypothesis_path, existing_path, '/')) or '/'
202
+
203
+ default_selector = first_non_empty((s.get('wait_for_selector') or '').strip(), (current_plan.get('wait_for_selector') or '').strip())
204
+ default_proof_plan = authored_proof_plan(s, reference, default_path, baselines, default_selector)
205
+ default_capture_script = authored_capture_script(s.get('capture_script'), default_selector)
206
+
207
+ supervisor_packet = s.get('supervisor_author_packet') or {}
208
+ if not isinstance(supervisor_packet, dict):
209
+ supervisor_packet = {}
210
+
211
+ provided_payload = {
212
+ 'proof_plan': first_non_empty(supervisor_packet.get('proof_plan'), s.get('proof_plan')),
213
+ 'capture_script': first_non_empty(supervisor_packet.get('capture_script'), s.get('capture_script')),
214
+ 'baseline_understanding_used': supervisor_packet.get('baseline_understanding_used') or recon_baseline_understanding(s),
215
+ 'refined_inputs': supervisor_packet.get('refined_inputs') or {},
216
+ 'rationale': supervisor_packet.get('rationale', s.get('supervisor_author_rationale', [])),
217
+ 'confidence': first_non_empty(supervisor_packet.get('confidence'), s.get('supervisor_author_confidence'), 'medium').lower(),
218
+ 'summary': first_non_empty(supervisor_packet.get('summary'), s.get('supervisor_author_summary')),
219
+ }
220
+
221
+ has_supervisor_packet = bool(provided_payload['proof_plan']) and bool(provided_payload['capture_script'])
222
+
223
+ if not has_supervisor_packet:
224
+ s['stage'] = 'author'
225
+ s['author_status'] = 'needs_supervisor_judgment'
226
+ s['proof_plan_status'] = 'needs_supervisor_judgment'
227
+ s['author_mode'] = 'supervisor_request'
228
+ s['author_model'] = 'supervising-agent'
229
+ s['author_confidence'] = 'pending'
230
+ s['author_rationale'] = []
231
+ s['author_warnings'] = []
232
+ s['author_runtime_model_hint'] = RUNTIME_MODEL_HINT
233
+ s['author_summary'] = 'Awaiting supervising agent proof packet for recon-confirmed route ' + default_path
234
+ s['proof_assessment'] = s.get('proof_assessment') or {}
235
+ s['author_request'] = author_request_payload(s, reference, baselines, current_plan, hypothesis, default_path, default_selector)
236
+ s['proof_plan_request'] = s['author_request']
237
+ save_state(s)
238
+
239
+ print('AUTHOR')
240
+ print('=' * 50)
241
+ print('Proof plan ready: no')
242
+ print('Capture script ready: no')
243
+ print('Authoring owner: supervising agent')
244
+ print('Target path draft: ' + default_path)
245
+ print('Wait for selector draft: ' + (default_selector or '(none)'))
246
+ print(json.dumps({
247
+ 'ok': True,
248
+ 'author_status': s['author_status'],
249
+ 'proof_plan_status': s['proof_plan_status'],
250
+ 'author_mode': s['author_mode'],
251
+ 'author_model': s['author_model'],
252
+ 'server_path': default_path,
253
+ 'wait_for_selector': default_selector,
254
+ }, indent=2))
255
+ raise SystemExit(0)
256
+
257
+ refined = provided_payload['refined_inputs'] if isinstance(provided_payload['refined_inputs'], dict) else {}
258
+ refined_path = normalize_path(first_non_empty(refined.get('server_path'), s.get('server_path'), default_path)) or '/'
259
+ refined_selector = first_non_empty(refined.get('wait_for_selector'), s.get('wait_for_selector'), default_selector)
260
+ refined_reference = first_non_empty(refined.get('reference'), reference) or reference
261
+ confidence = provided_payload['confidence'] if provided_payload['confidence'] in ('high', 'medium', 'low') else 'medium'
262
+ rationale = sanitize_rationale(provided_payload['rationale'])
263
+ summary = provided_payload['summary'] or 'Supervising agent supplied the proof packet from recon observations.'
264
+
265
+ authored_packet = {
266
+ 'proof_plan': provided_payload['proof_plan'],
267
+ 'capture_script': provided_payload['capture_script'],
268
+ 'baseline_understanding_used': provided_payload['baseline_understanding_used'] if isinstance(provided_payload['baseline_understanding_used'], dict) else {},
269
+ 'refined_inputs': {
270
+ 'server_path': refined_path,
271
+ 'wait_for_selector': refined_selector,
272
+ 'reference': refined_reference,
273
+ },
274
+ 'rationale': rationale,
275
+ 'confidence': confidence,
276
+ 'mode': 'supervising_agent',
277
+ 'model': ('supervising-agent:' + RUNTIME_MODEL_HINT) if RUNTIME_MODEL_HINT else 'supervising-agent',
278
+ 'summary': summary,
279
+ }
280
+
281
+ s['server_path'] = refined_path
282
+ if refined_selector:
283
+ s['wait_for_selector'] = refined_selector
284
+ elif s.get('wait_for_selector'):
285
+ s['wait_for_selector'] = ''
286
+
287
+ s['proof_plan'] = authored_packet['proof_plan']
288
+ s['capture_script'] = authored_packet['capture_script']
289
+ s['author_status'] = 'ready'
290
+ s['proof_plan_status'] = 'ready'
291
+ s['stage'] = 'author'
292
+ s['author_mode'] = 'supervising_agent'
293
+ s['author_model'] = authored_packet['model']
294
+ s['author_confidence'] = confidence
295
+ s['author_rationale'] = rationale
296
+ s['author_warnings'] = []
297
+ s['author_runtime_model_hint'] = RUNTIME_MODEL_HINT
298
+ s['author_packet'] = authored_packet
299
+ s['author_summary'] = summary
300
+ s['supervisor_author_packet'] = authored_packet
301
+ s['author_baseline_understanding_used'] = authored_packet['baseline_understanding_used']
302
+
303
+ authored_request = dict(author_request_payload(s, refined_reference, baselines, current_plan, hypothesis, refined_path, refined_selector))
304
+ authored_request.update({
305
+ 'status': 'ready',
306
+ 'authoring_mode': 'supervising_agent',
307
+ 'authoring_model': authored_packet['model'],
308
+ 'confidence': confidence,
309
+ 'rationale': rationale,
310
+ 'warnings': [],
311
+ 'runtime_model_hint': RUNTIME_MODEL_HINT,
312
+ 'refined_inputs': authored_packet['refined_inputs'],
313
+ 'authored_outputs': {
314
+ 'proof_plan': authored_packet['proof_plan'],
315
+ 'capture_script': authored_packet['capture_script'],
316
+ },
317
+ 'summary': summary,
318
+ })
319
+ s['author_request'] = authored_request
320
+ s['proof_plan_request'] = authored_request
321
+
322
+ save_state(s)
323
+
324
+ print('AUTHOR')
325
+ print('=' * 50)
326
+ print('Proof plan ready: yes')
327
+ print('Capture script ready: yes')
328
+ print('Authoring owner: supervising agent')
329
+ print('Authoring model hint: ' + s['author_model'])
330
+ print('Target path: ' + refined_path)
331
+ print('Wait for selector: ' + (refined_selector or '(none)'))
332
+ print('Reference: ' + str(refined_reference))
333
+ if rationale:
334
+ print('Rationale: ' + rationale[0])
335
+ print(json.dumps({
336
+ 'ok': True,
337
+ 'author_status': s['author_status'],
338
+ 'proof_plan_status': s['proof_plan_status'],
339
+ 'author_mode': s['author_mode'],
340
+ 'author_model': s['author_model'],
341
+ 'server_path': s.get('server_path', ''),
342
+ 'wait_for_selector': s.get('wait_for_selector', ''),
343
+ }, indent=2))
@@ -0,0 +1,63 @@
1
+ """Implement: confirm that code work now exists between recon and verify.
2
+
3
+ This stage does not make code changes itself. It records that implementation
4
+ has happened on the after worktree so verify does not run against an untouched
5
+ branch.
6
+ """
7
+
8
+ import json, os, sys
9
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
+ from util import load_state, save_state, git, shell_quote
11
+
12
+ s = load_state()
13
+ after_dir = (s.get('after_worktree') or '').strip()
14
+ if not after_dir or not os.path.exists(after_dir):
15
+ raise SystemExit('after_worktree not found. Run setup first.')
16
+
17
+ base_branch = s.get('base_branch', 'main')
18
+ base_ref = (s.get('before_ref') or '').strip() or ('origin/' + base_branch)
19
+ dirty = [ln for ln in git('git status --short', after_dir).stdout.splitlines() if ln.strip()]
20
+
21
+ diff_cmd = 'git diff --name-only ' + shell_quote(base_ref) + '...HEAD'
22
+ diff_result = git(diff_cmd, after_dir)
23
+ committed = [ln for ln in diff_result.stdout.splitlines() if ln.strip()]
24
+ if diff_result.returncode != 0:
25
+ fallback_base = base_branch
26
+ fallback_result = git('git diff --name-only ' + shell_quote(fallback_base) + '...HEAD', after_dir)
27
+ committed = [ln for ln in fallback_result.stdout.splitlines() if ln.strip()]
28
+ if diff_result.returncode != 0 and not committed:
29
+ fallback = git('git diff --name-only HEAD~1 HEAD', after_dir)
30
+ committed = [ln for ln in fallback.stdout.splitlines() if ln.strip()]
31
+
32
+ changed = []
33
+ for line in dirty + committed:
34
+ item = line.strip()
35
+ if item and item not in changed:
36
+ changed.append(item)
37
+
38
+ if not changed:
39
+ raise SystemExit('No implementation detected on the after worktree. Make the code changes, then rerun riddle-proof-implement.')
40
+
41
+ summary = 'Implementation detected in ' + str(len(changed)) + ' file(s): ' + ', '.join(changed[:8])
42
+ if len(changed) > 8:
43
+ summary += ', ...'
44
+
45
+ s['implementation_status'] = 'changes_detected'
46
+ s['implementation_summary'] = summary
47
+ s['changed_files'] = changed[:20]
48
+ s['stage'] = 'implement'
49
+ authored = bool((s.get('capture_script') or '').strip()) and bool((s.get('proof_plan') or '').strip())
50
+ if authored:
51
+ s['author_status'] = 'ready'
52
+ s['proof_plan_status'] = 'ready'
53
+ else:
54
+ s['author_status'] = s.get('author_status') or 'needs_authoring'
55
+ s['proof_plan_status'] = s.get('proof_plan_status') or 'needs_authoring'
56
+ save_state(s)
57
+
58
+ print('IMPLEMENT')
59
+ print('=' * 50)
60
+ print(summary)
61
+ if s.get('implementation_notes'):
62
+ print('Implementation notes: ' + s['implementation_notes'])
63
+ print(json.dumps({'ok': True, 'changed_files': changed[:20]}))
@@ -0,0 +1,246 @@
1
+ """Preflight: validate args and initialize state file.
2
+
3
+ Reads args from RIDDLE_PROOF_ARGS_FILE, defaulting to /tmp/riddle-proof-args.json.
4
+ Supports both static and server modes.
5
+ reference: 'prod', 'before', or 'both' (default: 'both')
6
+ capture_script: optional at setup and recon; required before verify.
7
+ """
8
+
9
+ import json, os, re, time, uuid, sys
10
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
+ from util import STATE_FILE, ARGS_FILE, save_state, invoke, load_package_json
12
+
13
+ def truthy(value):
14
+ return str(value or '').strip().lower() in ('1', 'true', 'yes', 'y', 'on')
15
+
16
+
17
+ def parse_json_arg(key, expected, default):
18
+ raw = s.get(key)
19
+ if raw in (None, ''):
20
+ return default
21
+ if isinstance(raw, expected):
22
+ return raw
23
+ if not isinstance(raw, str):
24
+ raise SystemExit(key + ' must be JSON ' + '/'.join(t.__name__ for t in expected) + '.')
25
+ text = raw.strip()
26
+ if not text:
27
+ return default
28
+ try:
29
+ parsed = json.loads(text)
30
+ except Exception as e:
31
+ raise SystemExit(key + ' is not valid JSON: ' + str(e))
32
+ if not isinstance(parsed, expected):
33
+ raise SystemExit(key + ' must decode to ' + '/'.join(t.__name__ for t in expected) + '.')
34
+ return parsed
35
+
36
+ args_file = ARGS_FILE
37
+ if not os.path.exists(args_file):
38
+ raise SystemExit('No args file. Lobster wrapper must write ' + args_file + ' first.')
39
+
40
+ with open(args_file) as f:
41
+ s = json.load(f)
42
+
43
+ mode = (s.get('mode') or '').strip().lower()
44
+ reference = s.get('reference', 'both')
45
+ reference_note = ''
46
+ verification_mode = (s.get('verification_mode') or 'proof').strip() or 'proof'
47
+ s['verification_mode'] = verification_mode
48
+ s['success_criteria'] = (s.get('success_criteria') or '').strip()
49
+ raw_assertions = (s.get('assertions_json') or '').strip()
50
+ allow_static_preview_fallback = str(s.get('allow_static_preview_fallback') or '').strip().lower() in ('1', 'true', 'yes', 'y', 'on')
51
+ s['allow_static_preview_fallback'] = allow_static_preview_fallback
52
+ s['leave_draft'] = truthy(s.get('leave_draft'))
53
+ for key in ('discord_channel', 'discord_thread_id', 'discord_message_id', 'discord_source_url'):
54
+ s[key] = (s.get(key) or '').strip()
55
+
56
+ discord_url_match = re.search(r'discord(?:app)?\.com/channels/[^/]+/([^/?#]+)/([^/?#]+)', s.get('discord_source_url') or '')
57
+ if discord_url_match:
58
+ discord_container_id, discord_message_id = discord_url_match.groups()
59
+ if not s.get('discord_message_id'):
60
+ s['discord_message_id'] = discord_message_id
61
+ if not s.get('discord_thread_id') and (not s.get('discord_channel') or discord_container_id != s.get('discord_channel')):
62
+ # The URL's channel component is the exact message container. If it
63
+ # differs from the parent channel, it is the thread ID; if no parent is
64
+ # known, it still works as a POST target.
65
+ s['discord_thread_id'] = discord_container_id
66
+
67
+ # Validate reference
68
+ if reference not in ('prod', 'before', 'both'):
69
+ raise SystemExit('Invalid reference: ' + reference + '. Must be prod, before, or both.')
70
+ s['reference'] = reference
71
+
72
+ # Infer a reasonable commit title during setup instead of forcing the caller to
73
+ # fill boilerplate that can be derived from the requested change.
74
+ if not (s.get('commit_message') or '').strip():
75
+ s['commit_message'] = (s.get('change_request') or '').strip()
76
+
77
+ # Setup should not block on a missing production URL. If prod comparison was
78
+ # requested but prod_url is not known yet, continue with a before-only setup so
79
+ # the repo homework can happen first.
80
+ if reference in ('prod', 'both') and not (s.get('prod_url') or '').strip():
81
+ s['requested_reference'] = reference
82
+ reference = 'before'
83
+ s['reference'] = reference
84
+ reference_note = 'prod_url not provided; setup will continue with reference=before until prod is known.'
85
+
86
+ # Parse optional assertions JSON
87
+ parsed_assertions = None
88
+ if raw_assertions:
89
+ try:
90
+ parsed_assertions = json.loads(raw_assertions)
91
+ except Exception as e:
92
+ raise SystemExit('assertions_json is not valid JSON: ' + str(e))
93
+ s['parsed_assertions'] = parsed_assertions
94
+
95
+ # Generate branch if not provided. The riddle-proof/* namespace is reserved for
96
+ # temporary proof worktrees, so never let a user-supplied branch use it as the
97
+ # real PR branch.
98
+ requested_branch = (s.get('branch') or '').strip()
99
+ if not requested_branch or requested_branch.startswith('riddle-proof/'):
100
+ slug = re.sub(r'[^a-z0-9]+', '-', (s.get('change_request') or 'proof-check').lower())[:34].strip('-') or 'proof-check'
101
+ s['branch'] = 'agent/openclaw/' + slug + '-' + uuid.uuid4().hex[:6]
102
+ if requested_branch:
103
+ s['branch_rewritten_from'] = requested_branch
104
+ requested_target_branch = (s.get('target_branch') or '').strip()
105
+ if requested_target_branch.startswith('riddle-proof/'):
106
+ s['target_branch_rewritten_from'] = requested_target_branch
107
+ requested_target_branch = ''
108
+ s['target_branch'] = (requested_target_branch or s.get('branch') or '').strip()
109
+ s['ship_target_branch'] = s['target_branch']
110
+ if s['target_branch'].startswith('riddle-proof/'):
111
+ raise SystemExit('Invalid target_branch: riddle-proof/* is reserved for temporary proof worktrees.')
112
+
113
+ # Validate required fields (common)
114
+ missing = []
115
+ for k in ('repo', 'change_request', 'commit_message'):
116
+ if not s.get(k):
117
+ missing.append(k)
118
+
119
+ # prod_url required only once prod is actively part of the comparison
120
+ if reference in ('prod', 'both') and not s.get('prod_url', '').strip():
121
+ missing.append('prod_url (required when reference=' + reference + ')')
122
+
123
+ # Mode-specific validation
124
+ if mode == 'server':
125
+ for k in ('server_image', 'server_command', 'server_port'):
126
+ if not s.get(k):
127
+ missing.append(k)
128
+
129
+ # Derived fields
130
+ repo_short = s['repo'].split('/')[-1] if s.get('repo') else ''
131
+ s['repo_short'] = repo_short
132
+ base_branch = (s.get('base_branch') or 'main').strip() or 'main'
133
+ s['base_branch'] = base_branch
134
+ if not (s.get('before_ref') or '').strip() and not (s.get('base_ref') or '').strip():
135
+ s['before_ref'] = 'origin/' + base_branch
136
+ workspace = os.environ.get('OPENCLAW_WORKSPACE')
137
+ if not workspace:
138
+ for candidate in ('/mnt/efs/openclaw/workspace', os.path.expanduser('~/.openclaw/workspace')):
139
+ if os.path.exists(candidate):
140
+ workspace = candidate
141
+ break
142
+ workspace = workspace or os.path.expanduser('~/.openclaw/workspace')
143
+ s['repo_dir'] = s.get('repo_dir') or (workspace + '/' + repo_short if repo_short else '')
144
+
145
+ if not mode:
146
+ repo_dir = s.get('repo_dir', '')
147
+ pkg = load_package_json(repo_dir) if repo_dir else {}
148
+ scripts = pkg.get('scripts') or {}
149
+ script_blob = ' '.join(str(scripts.get(k, '')).lower() for k in ('start', 'preview', 'dev'))
150
+ mode = 'static' if 'vite' in script_blob or 'react-scripts' in script_blob else 'server'
151
+ s['mode'] = mode or 'static'
152
+ mode = s['mode']
153
+
154
+ run_id = time.strftime('%Y%m%dT%H%M%SZ', time.gmtime()) + '-' + uuid.uuid4().hex[:8]
155
+ s['run_id'] = run_id
156
+ s['stage'] = 'preflight'
157
+ s['status'] = 'ready' if not missing else 'needs_input'
158
+ s['missing'] = missing
159
+
160
+ # Auth context can be supplied directly for public-plugin use, while use_auth
161
+ # remains a private/configured Cognito helper for Riddle-owned environments.
162
+ explicit_local_storage = parse_json_arg('auth_localStorage_json', (dict,), {})
163
+ explicit_cookies = parse_json_arg('auth_cookies_json', (dict, list), {})
164
+ explicit_headers = parse_json_arg('auth_headers_json', (dict,), {})
165
+ s['auth_explicit_localStorage'] = explicit_local_storage
166
+ s['auth_cookies'] = explicit_cookies
167
+ s['auth_headers'] = explicit_headers
168
+ s['auth_localStorage'] = dict(explicit_local_storage)
169
+
170
+ if truthy(s.get('use_auth')):
171
+ print('Fetching Cognito auth tokens...')
172
+ auth = invoke('auth_cognito_tokens', {}, timeout=60)
173
+ if auth.get('ok') and auth.get('localStorage'):
174
+ merged_local_storage = dict(auth['localStorage'])
175
+ merged_local_storage.update(explicit_local_storage)
176
+ s['auth_localStorage'] = merged_local_storage
177
+ print('Auth tokens fetched (' + str(len(auth['localStorage'])) + ' keys)')
178
+ else:
179
+ raise SystemExit('Failed to fetch auth tokens for use_auth=true: ' + str(auth.get('error', 'unknown')))
180
+
181
+ # Init capture/proof fields
182
+ s['before_cdn'] = ''
183
+ s['after_cdn'] = ''
184
+ s['prod_cdn'] = ''
185
+ s['before_preview_id'] = ''
186
+ s['after_preview_id'] = ''
187
+ s['before_worktree'] = ''
188
+ s['after_worktree'] = ''
189
+ s['after_worktree_branch'] = ''
190
+ s['pr_url'] = s.get('pr_url', '')
191
+ s['pr_number'] = s.get('pr_number', '')
192
+ s['recon_results'] = {}
193
+ s['verify_results'] = {}
194
+ s['capture_diagnostics'] = []
195
+ s['review_passed'] = False
196
+ s['finalized'] = False
197
+ s['proof_summary'] = ''
198
+ s['proof_plan'] = (s.get('proof_plan') or '').strip()
199
+ s['proof_plan_request'] = s.get('proof_plan_request') or {}
200
+ s['author_request'] = s.get('author_request') or {}
201
+ s['author_summary'] = ''
202
+ authored = bool((s.get('capture_script') or '').strip()) and bool((s.get('proof_plan') or '').strip())
203
+ s['author_status'] = 'ready' if authored else 'pending_recon'
204
+ s['proof_plan_status'] = 'ready' if authored else 'pending_recon'
205
+ s['recon_summary'] = ''
206
+ s['recon_hypothesis'] = s.get('recon_hypothesis') or {}
207
+ s['assertion_status'] = 'not_run'
208
+ s['merge_recommendation'] = ''
209
+ s['evidence_notes'] = []
210
+ s['implementation_status'] = 'pending_recon'
211
+ s['implementation_summary'] = ''
212
+ s['implementation_notes'] = (s.get('implementation_notes') or '').strip()
213
+
214
+ print('RIDDLE PROOF — PREFLIGHT (' + mode.upper() + ' / ' + reference.upper() + ')')
215
+ print('=' * 50)
216
+ display_keys = ['repo', 'branch', 'target_branch', 'ship_target_branch', 'base_branch', 'before_ref', 'change_request', 'commit_message',
217
+ 'reference', 'verification_mode', 'success_criteria', 'prod_url', 'build_command',
218
+ 'allow_static_preview_fallback']
219
+ if mode == 'server':
220
+ display_keys += ['server_image', 'server_command', 'server_port', 'server_path']
221
+ if s.get('auth_localStorage'):
222
+ print('auth: localStorage loaded (' + str(len(s['auth_localStorage'])) + ' keys)')
223
+ if s.get('auth_cookies'):
224
+ print('auth: cookies supplied')
225
+ if s.get('auth_headers'):
226
+ print('auth: headers supplied (' + str(len(s['auth_headers'])) + ' keys)')
227
+ for k in display_keys:
228
+ v = str(s.get(k, ''))
229
+ if len(v) > 120:
230
+ v = v[:120] + '...'
231
+ print(k + ': ' + v)
232
+ if parsed_assertions is not None:
233
+ print('assertions_json: parsed')
234
+ if not (s.get('capture_script') or '').strip():
235
+ print('NOTE: capture_script can be added later after recon and before verify.')
236
+ if reference_note:
237
+ print('NOTE: ' + reference_note)
238
+ if missing:
239
+ print('MISSING: ' + ', '.join(missing))
240
+ print('=' * 50)
241
+
242
+ save_state(s)
243
+
244
+ if missing:
245
+ raise SystemExit('Missing required fields: ' + ', '.join(missing))
246
+ print(json.dumps({'ok': True, 'run_id': run_id, 'mode': mode, 'reference': reference, 'verification_mode': verification_mode}))