@riddledc/riddle-proof 0.5.1 → 0.5.3
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/README.md +2 -2
- package/dist/chunk-3MHFLQKG.js +853 -0
- package/dist/{chunk-LVP22WE4.js → chunk-MLJJIKTP.js} +56 -3
- package/dist/engine-harness.cjs +2659 -24
- package/dist/engine-harness.js +1 -1
- package/dist/index.cjs +2666 -31
- package/dist/index.js +2 -2
- package/dist/proof-run-core.cjs +909 -0
- package/dist/proof-run-core.d.cts +280 -0
- package/dist/proof-run-core.d.ts +280 -0
- package/dist/proof-run-core.js +48 -0
- package/dist/proof-run-engine.cjs +2602 -0
- package/dist/proof-run-engine.d.cts +677 -0
- package/dist/proof-run-engine.d.ts +677 -0
- package/dist/proof-run-engine.js +1752 -0
- package/lib/workspace-core.mjs +391 -0
- package/package.json +15 -3
- package/runtime/lib/author.py +343 -0
- package/runtime/lib/implement.py +63 -0
- package/runtime/lib/preflight.py +259 -0
- package/runtime/lib/recon.py +1048 -0
- package/runtime/lib/riddle_core_call.mjs +151 -0
- package/runtime/lib/setup.py +387 -0
- package/runtime/lib/ship.py +834 -0
- package/runtime/lib/util.py +673 -0
- package/runtime/lib/verify.py +1223 -0
- package/runtime/pipelines/riddle-proof-author.lobster +28 -0
- package/runtime/pipelines/riddle-proof-implement.lobster +26 -0
- package/runtime/pipelines/riddle-proof-recon.lobster +79 -0
- package/runtime/pipelines/riddle-proof-setup.lobster +141 -0
- package/runtime/pipelines/riddle-proof-ship.lobster +36 -0
- package/runtime/pipelines/riddle-proof-verify.lobster +74 -0
- 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,259 @@
|
|
|
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
|
+
# The TypeScript harness writes runtime observability fields before Lobster
|
|
243
|
+
# starts. Preflight initializes the main state file, so preserve those fields
|
|
244
|
+
# rather than making status polling go blind during setup.
|
|
245
|
+
if os.path.exists(STATE_FILE):
|
|
246
|
+
try:
|
|
247
|
+
with open(STATE_FILE) as existing_state_file:
|
|
248
|
+
existing_state = json.load(existing_state_file)
|
|
249
|
+
except Exception:
|
|
250
|
+
existing_state = {}
|
|
251
|
+
for runtime_key in ('current_runtime_step', 'last_runtime_step', 'runtime_events', 'runtime_updated_at'):
|
|
252
|
+
if runtime_key in existing_state:
|
|
253
|
+
s[runtime_key] = existing_state[runtime_key]
|
|
254
|
+
|
|
255
|
+
save_state(s)
|
|
256
|
+
|
|
257
|
+
if missing:
|
|
258
|
+
raise SystemExit('Missing required fields: ' + ', '.join(missing))
|
|
259
|
+
print(json.dumps({'ok': True, 'run_id': run_id, 'mode': mode, 'reference': reference, 'verification_mode': verification_mode}))
|