@riddledc/riddle-proof 0.5.11 → 0.5.13
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 +7 -0
- package/package.json +1 -1
- package/runtime/lib/recon.py +19 -0
- package/runtime/lib/setup.py +116 -0
- package/runtime/lib/util.py +211 -1
- package/runtime/lib/verify.py +11 -0
package/README.md
CHANGED
|
@@ -91,6 +91,13 @@ Set `RIDDLE_PROOF_WORKTREE_ROOT` to choose an explicit location. Set
|
|
|
91
91
|
`RIDDLE_PROOF_USE_WORKSPACE_WORKTREE_ROOT=1` to keep the previous behavior of
|
|
92
92
|
placing proof worktrees next to the active repository.
|
|
93
93
|
|
|
94
|
+
When local scratch storage is low, setup prunes stale
|
|
95
|
+
`riddle-proof-*` worktrees from the scratch root before creating the next run.
|
|
96
|
+
This preserves the dependency cache for speed while avoiding old failed runs
|
|
97
|
+
filling `/tmp`. Set `RIDDLE_PROOF_KEEP_SCRATCH_WORKTREES=1` to disable that
|
|
98
|
+
cleanup for debugging, or tune the low-space threshold with
|
|
99
|
+
`RIDDLE_PROOF_MIN_SCRATCH_FREE_MB`.
|
|
100
|
+
|
|
94
101
|
## Capture Diagnostics
|
|
95
102
|
|
|
96
103
|
`@riddledc/riddle-proof/diagnostics` standardizes the evidence contract around
|
package/package.json
CHANGED
package/runtime/lib/recon.py
CHANGED
|
@@ -783,6 +783,25 @@ elif plan_history:
|
|
|
783
783
|
'changes': diff_plan(plan_history[-1], current_plan),
|
|
784
784
|
'reason': 'Resumed recon with the current state inputs.',
|
|
785
785
|
})
|
|
786
|
+
|
|
787
|
+
capture_hint = s.get('capture_hint') if isinstance(s.get('capture_hint'), dict) else {}
|
|
788
|
+
selected_hint = capture_hint.get('selected') if isinstance(capture_hint.get('selected'), dict) else {}
|
|
789
|
+
if capture_hint.get('applied') and selected_hint:
|
|
790
|
+
fallback_changes = {}
|
|
791
|
+
hinted_path = str(selected_hint.get('server_path') or '').strip()
|
|
792
|
+
hinted_selector = str(selected_hint.get('wait_for_selector') or '').strip()
|
|
793
|
+
if hinted_path and current_plan.get('target_path') != hinted_path:
|
|
794
|
+
fallback_changes['server_path'] = {'from': hinted_path, 'to': current_plan.get('target_path')}
|
|
795
|
+
if hinted_selector and current_plan.get('wait_for_selector') != hinted_selector:
|
|
796
|
+
fallback_changes['wait_for_selector'] = {'from': hinted_selector, 'to': current_plan.get('wait_for_selector')}
|
|
797
|
+
if fallback_changes:
|
|
798
|
+
capture_hint['fallback_triggered'] = True
|
|
799
|
+
capture_hint['fallback_reason'] = (
|
|
800
|
+
str(previous_assessment.get('decision') or '').strip() if has_previous_assessment else 'plan_refined'
|
|
801
|
+
) or 'plan_refined'
|
|
802
|
+
capture_hint['fallback_changes'] = fallback_changes
|
|
803
|
+
s['capture_hint'] = capture_hint
|
|
804
|
+
|
|
786
805
|
plan_history.append(current_plan)
|
|
787
806
|
|
|
788
807
|
summary_bits = []
|
package/runtime/lib/setup.py
CHANGED
|
@@ -9,6 +9,7 @@ local temp storage by default:
|
|
|
9
9
|
import json, subprocess as sp, os, sys, shutil, time, tempfile
|
|
10
10
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
11
|
from util import load_state, save_state, git, shell_quote
|
|
12
|
+
from util import apply_capture_hint
|
|
12
13
|
|
|
13
14
|
s = load_state()
|
|
14
15
|
repo = s['repo']
|
|
@@ -170,6 +171,113 @@ def resolve_worktree_root(repo_dir):
|
|
|
170
171
|
return os.path.join(tempfile.gettempdir(), '.riddle-proof-worktrees')
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
def env_flag(name, default=False):
|
|
175
|
+
raw = os.environ.get(name, '').strip().lower()
|
|
176
|
+
if raw in ('1', 'true', 'yes', 'on'):
|
|
177
|
+
return True
|
|
178
|
+
if raw in ('0', 'false', 'no', 'off'):
|
|
179
|
+
return False
|
|
180
|
+
return default
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def env_int(name, default):
|
|
184
|
+
raw = os.environ.get(name, '').strip()
|
|
185
|
+
try:
|
|
186
|
+
value = int(raw)
|
|
187
|
+
except Exception:
|
|
188
|
+
return default
|
|
189
|
+
return value if value > 0 else default
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def disk_free_bytes(path):
|
|
193
|
+
probe = path
|
|
194
|
+
while probe and not os.path.exists(probe):
|
|
195
|
+
parent = os.path.dirname(probe)
|
|
196
|
+
if parent == probe:
|
|
197
|
+
break
|
|
198
|
+
probe = parent
|
|
199
|
+
try:
|
|
200
|
+
return shutil.disk_usage(probe or tempfile.gettempdir()).free
|
|
201
|
+
except Exception:
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def prune_scratch_worktrees(worktree_root, keep_dirs, repo_dir):
|
|
206
|
+
report = {
|
|
207
|
+
'requested': True,
|
|
208
|
+
'worktree_root': worktree_root,
|
|
209
|
+
'removed': [],
|
|
210
|
+
'errors': [],
|
|
211
|
+
}
|
|
212
|
+
if env_flag('RIDDLE_PROOF_KEEP_SCRATCH_WORKTREES', False):
|
|
213
|
+
report['skipped'] = 'RIDDLE_PROOF_KEEP_SCRATCH_WORKTREES'
|
|
214
|
+
return report
|
|
215
|
+
if not worktree_root:
|
|
216
|
+
report['skipped'] = 'missing_worktree_root'
|
|
217
|
+
return report
|
|
218
|
+
|
|
219
|
+
root = os.path.abspath(os.path.expanduser(worktree_root))
|
|
220
|
+
temp_root = os.path.abspath(tempfile.gettempdir())
|
|
221
|
+
if root in ('/', temp_root) or not root.endswith('.riddle-proof-worktrees'):
|
|
222
|
+
report['skipped'] = 'unsafe_worktree_root'
|
|
223
|
+
return report
|
|
224
|
+
if not os.path.isdir(root):
|
|
225
|
+
report['skipped'] = 'worktree_root_missing'
|
|
226
|
+
return report
|
|
227
|
+
|
|
228
|
+
min_free_bytes = env_int('RIDDLE_PROOF_MIN_SCRATCH_FREE_MB', 2048) * 1024 * 1024
|
|
229
|
+
free_before = disk_free_bytes(root)
|
|
230
|
+
report['free_before_bytes'] = free_before
|
|
231
|
+
report['min_free_bytes'] = min_free_bytes
|
|
232
|
+
if free_before >= min_free_bytes:
|
|
233
|
+
report['skipped'] = 'enough_free_space'
|
|
234
|
+
return report
|
|
235
|
+
|
|
236
|
+
keep = set(os.path.abspath(os.path.expanduser(p)) for p in keep_dirs if p)
|
|
237
|
+
candidates = []
|
|
238
|
+
for name in os.listdir(root):
|
|
239
|
+
if not name.startswith('riddle-proof-'):
|
|
240
|
+
continue
|
|
241
|
+
path = os.path.join(root, name)
|
|
242
|
+
resolved = os.path.abspath(path)
|
|
243
|
+
if resolved in keep or not os.path.isdir(path):
|
|
244
|
+
continue
|
|
245
|
+
try:
|
|
246
|
+
mtime = os.path.getmtime(path)
|
|
247
|
+
except Exception:
|
|
248
|
+
mtime = 0
|
|
249
|
+
candidates.append((mtime, path))
|
|
250
|
+
candidates.sort()
|
|
251
|
+
|
|
252
|
+
for _, path in candidates:
|
|
253
|
+
if disk_free_bytes(root) >= min_free_bytes:
|
|
254
|
+
break
|
|
255
|
+
removed_by_git = False
|
|
256
|
+
git_error = ''
|
|
257
|
+
if repo_dir and os.path.exists(os.path.join(repo_dir, '.git')):
|
|
258
|
+
remove_result = sp.run(
|
|
259
|
+
'git worktree remove --force ' + shell_quote(path),
|
|
260
|
+
shell=True,
|
|
261
|
+
cwd=repo_dir,
|
|
262
|
+
capture_output=True,
|
|
263
|
+
text=True,
|
|
264
|
+
)
|
|
265
|
+
removed_by_git = remove_result.returncode == 0
|
|
266
|
+
if remove_result.returncode != 0 and os.path.exists(path):
|
|
267
|
+
git_error = (remove_result.stderr or remove_result.stdout or '')[:300]
|
|
268
|
+
if os.path.exists(path):
|
|
269
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
270
|
+
if not os.path.exists(path):
|
|
271
|
+
report['removed'].append({'path': path, 'via': 'git' if removed_by_git else 'filesystem'})
|
|
272
|
+
elif git_error:
|
|
273
|
+
report['errors'].append({'path': path, 'git_error': git_error})
|
|
274
|
+
|
|
275
|
+
if repo_dir and os.path.exists(os.path.join(repo_dir, '.git')):
|
|
276
|
+
git('git worktree prune', repo_dir)
|
|
277
|
+
report['free_after_bytes'] = disk_free_bytes(root)
|
|
278
|
+
return report
|
|
279
|
+
|
|
280
|
+
|
|
173
281
|
def cleanup_legacy_branch_worktrees(repo_dir, branch_name):
|
|
174
282
|
if not repo_dir or not os.path.exists(os.path.join(repo_dir, '.git')):
|
|
175
283
|
return
|
|
@@ -367,9 +475,17 @@ s['branch'] = branch
|
|
|
367
475
|
s['target_branch'] = target_branch
|
|
368
476
|
s['ship_target_branch'] = target_branch
|
|
369
477
|
s['worktree_root'] = WORKTREE_ROOT
|
|
478
|
+
capture_hint = apply_capture_hint(s)
|
|
479
|
+
if capture_hint and capture_hint.get('applied_fields'):
|
|
480
|
+
print('Applied last-good capture hint: ' + ', '.join(capture_hint.get('applied_fields') or []))
|
|
370
481
|
save_state(s)
|
|
371
482
|
print('Prepared workspace via ' + setup.get('source', 'workspace_core') + ': ' + repo_dir)
|
|
372
483
|
os.makedirs(WORKTREE_ROOT, exist_ok=True)
|
|
484
|
+
scratch_cleanup = prune_scratch_worktrees(WORKTREE_ROOT, (BEFORE_DIR, AFTER_DIR), repo_dir)
|
|
485
|
+
if scratch_cleanup.get('removed') or scratch_cleanup.get('errors'):
|
|
486
|
+
print('Scratch cleanup: removed ' + str(len(scratch_cleanup.get('removed') or [])) + ' stale proof worktree(s)')
|
|
487
|
+
s['scratch_cleanup'] = scratch_cleanup
|
|
488
|
+
save_state(s)
|
|
373
489
|
cleanup_legacy_branch_worktrees(repo_dir, base_branch)
|
|
374
490
|
|
|
375
491
|
# Clean any stale worktrees for this run and the legacy fixed paths
|
package/runtime/lib/util.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Shared helpers for Riddle Proof pipeline."""
|
|
2
2
|
|
|
3
|
-
import json, subprocess as sp,
|
|
3
|
+
import hashlib, json, os, re, shlex, subprocess as sp, tempfile, time
|
|
4
4
|
from urllib.parse import urljoin
|
|
5
5
|
from urllib.request import urlopen
|
|
6
6
|
|
|
@@ -18,6 +18,8 @@ CAPTURE_ARTIFACT_JSON_LIMIT = 256 * 1024
|
|
|
18
18
|
_JSON_ARTIFACT_CACHE = {}
|
|
19
19
|
CAPTURE_DIAGNOSTIC_VERSION = 'riddle-proof.capture-diagnostic.v1'
|
|
20
20
|
DEBUG_STRING_LIMIT = 2000
|
|
21
|
+
CAPTURE_HINT_CACHE_VERSION = 'riddle-proof.capture-hints.v1'
|
|
22
|
+
CAPTURE_HINT_CACHE_LIMIT = 12
|
|
21
23
|
SENSITIVE_KEY_FRAGMENTS = (
|
|
22
24
|
'authorization',
|
|
23
25
|
'apikey',
|
|
@@ -29,6 +31,14 @@ SENSITIVE_KEY_FRAGMENTS = (
|
|
|
29
31
|
'secret',
|
|
30
32
|
'token',
|
|
31
33
|
)
|
|
34
|
+
HINT_TOKEN_STOPWORDS = {
|
|
35
|
+
'about', 'after', 'agent', 'agents', 'around', 'before', 'browser', 'change',
|
|
36
|
+
'changes', 'clarify', 'component', 'copy', 'debug', 'default', 'evidence',
|
|
37
|
+
'flow', 'homepage', 'improve', 'just', 'main', 'make', 'need', 'normal',
|
|
38
|
+
'page', 'proof', 'report', 'results', 'review', 'run', 'screen', 'script',
|
|
39
|
+
'small', 'status', 'text', 'tiny', 'update', 'user', 'verify', 'visible',
|
|
40
|
+
'workflow',
|
|
41
|
+
}
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
def load_state():
|
|
@@ -41,6 +51,206 @@ def save_state(s):
|
|
|
41
51
|
json.dump(s, f, indent=2)
|
|
42
52
|
|
|
43
53
|
|
|
54
|
+
def request_shape_tokens(state, limit=8):
|
|
55
|
+
haystack = ' '.join([
|
|
56
|
+
str(state.get('change_request') or ''),
|
|
57
|
+
str(state.get('context') or ''),
|
|
58
|
+
str(state.get('success_criteria') or ''),
|
|
59
|
+
]).lower()
|
|
60
|
+
tokens = []
|
|
61
|
+
for word in re.findall(r'[a-z0-9]+', haystack):
|
|
62
|
+
if len(word) < 4 or word in HINT_TOKEN_STOPWORDS or word in tokens:
|
|
63
|
+
continue
|
|
64
|
+
tokens.append(word)
|
|
65
|
+
if len(tokens) >= limit:
|
|
66
|
+
break
|
|
67
|
+
return tokens
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def capture_hint_cache_path(state):
|
|
71
|
+
repo_key = str(state.get('repo') or state.get('repo_dir') or '').strip()
|
|
72
|
+
if not repo_key:
|
|
73
|
+
return ''
|
|
74
|
+
digest = hashlib.sha1(repo_key.encode('utf-8')).hexdigest()
|
|
75
|
+
return os.path.join(tempfile.gettempdir(), '.riddle-proof-capture-hints', digest + '.json')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_capture_hint_cache(state):
|
|
79
|
+
cache_path = capture_hint_cache_path(state)
|
|
80
|
+
if not cache_path or not os.path.exists(cache_path):
|
|
81
|
+
return ({'version': CAPTURE_HINT_CACHE_VERSION, 'hints': []}, cache_path)
|
|
82
|
+
try:
|
|
83
|
+
with open(cache_path) as f:
|
|
84
|
+
payload = json.load(f)
|
|
85
|
+
except Exception:
|
|
86
|
+
return ({'version': CAPTURE_HINT_CACHE_VERSION, 'hints': []}, cache_path)
|
|
87
|
+
if not isinstance(payload, dict):
|
|
88
|
+
return ({'version': CAPTURE_HINT_CACHE_VERSION, 'hints': []}, cache_path)
|
|
89
|
+
hints = payload.get('hints') if isinstance(payload.get('hints'), list) else []
|
|
90
|
+
payload['version'] = CAPTURE_HINT_CACHE_VERSION
|
|
91
|
+
payload['hints'] = hints
|
|
92
|
+
return payload, cache_path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def select_capture_hint(state):
|
|
96
|
+
payload, cache_path = load_capture_hint_cache(state)
|
|
97
|
+
hints = payload.get('hints') or []
|
|
98
|
+
current_tokens = request_shape_tokens(state)
|
|
99
|
+
current_mode = str(state.get('verification_mode') or '').strip().lower()
|
|
100
|
+
scored = []
|
|
101
|
+
for hint in hints:
|
|
102
|
+
if not isinstance(hint, dict):
|
|
103
|
+
continue
|
|
104
|
+
server_path = str(hint.get('server_path') or '').strip()
|
|
105
|
+
wait_for_selector = str(hint.get('wait_for_selector') or '').strip()
|
|
106
|
+
if not server_path and not wait_for_selector:
|
|
107
|
+
continue
|
|
108
|
+
hint_mode = str(hint.get('verification_mode') or '').strip().lower()
|
|
109
|
+
hint_tokens = [
|
|
110
|
+
str(item).strip().lower()
|
|
111
|
+
for item in (hint.get('request_tokens') or [])
|
|
112
|
+
if str(item).strip()
|
|
113
|
+
]
|
|
114
|
+
matched_tokens = [token for token in current_tokens if token in hint_tokens]
|
|
115
|
+
score = len(matched_tokens) * 3
|
|
116
|
+
if current_mode and hint_mode == current_mode:
|
|
117
|
+
score += 2
|
|
118
|
+
if score <= 0:
|
|
119
|
+
continue
|
|
120
|
+
scored.append({
|
|
121
|
+
'score': score,
|
|
122
|
+
'matched_tokens': matched_tokens,
|
|
123
|
+
'selection_reason': 'token_overlap_and_mode' if matched_tokens and current_mode and hint_mode == current_mode else (
|
|
124
|
+
'token_overlap' if matched_tokens else 'verification_mode_match'
|
|
125
|
+
),
|
|
126
|
+
'hint': hint,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if not scored:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
scored.sort(
|
|
133
|
+
key=lambda item: (
|
|
134
|
+
int(item['score']),
|
|
135
|
+
str(item['hint'].get('saved_at') or ''),
|
|
136
|
+
),
|
|
137
|
+
reverse=True,
|
|
138
|
+
)
|
|
139
|
+
selected = scored[0]
|
|
140
|
+
return {
|
|
141
|
+
'cache_path': cache_path,
|
|
142
|
+
'available_count': len(hints),
|
|
143
|
+
'score': selected['score'],
|
|
144
|
+
'matched_tokens': selected['matched_tokens'],
|
|
145
|
+
'selection_reason': selected['selection_reason'],
|
|
146
|
+
'hint': selected['hint'],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def apply_capture_hint(state):
|
|
151
|
+
selected = select_capture_hint(state)
|
|
152
|
+
if not selected:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
hint = selected['hint']
|
|
156
|
+
applied_fields = []
|
|
157
|
+
server_path = str(hint.get('server_path') or '').strip()
|
|
158
|
+
wait_for_selector = str(hint.get('wait_for_selector') or '').strip()
|
|
159
|
+
|
|
160
|
+
if server_path and not str(state.get('server_path') or '').strip():
|
|
161
|
+
state['server_path'] = server_path
|
|
162
|
+
state['server_path_source'] = 'hint_cache'
|
|
163
|
+
applied_fields.append('server_path')
|
|
164
|
+
if wait_for_selector and not str(state.get('wait_for_selector') or '').strip():
|
|
165
|
+
state['wait_for_selector'] = wait_for_selector
|
|
166
|
+
state['wait_for_selector_source'] = 'hint_cache'
|
|
167
|
+
applied_fields.append('wait_for_selector')
|
|
168
|
+
|
|
169
|
+
if not applied_fields:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
state['capture_hint'] = {
|
|
173
|
+
'source': 'hint_cache',
|
|
174
|
+
'cache_path': selected['cache_path'],
|
|
175
|
+
'applied': True,
|
|
176
|
+
'applied_fields': applied_fields,
|
|
177
|
+
'matched_tokens': selected['matched_tokens'],
|
|
178
|
+
'selection_reason': selected['selection_reason'],
|
|
179
|
+
'available_count': selected['available_count'],
|
|
180
|
+
'selected': {
|
|
181
|
+
'saved_at': str(hint.get('saved_at') or ''),
|
|
182
|
+
'verification_mode': str(hint.get('verification_mode') or ''),
|
|
183
|
+
'server_path': server_path,
|
|
184
|
+
'wait_for_selector': wait_for_selector,
|
|
185
|
+
'observed_path': str(hint.get('observed_path') or ''),
|
|
186
|
+
'proof_profile_name': str(hint.get('proof_profile_name') or ''),
|
|
187
|
+
'request_tokens': hint.get('request_tokens') or [],
|
|
188
|
+
},
|
|
189
|
+
'fallback_triggered': False,
|
|
190
|
+
}
|
|
191
|
+
return state['capture_hint']
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def record_successful_capture_hint(state, server_path='', wait_for_selector='', observed_path='', source_stage='verify', success_signal=''):
|
|
195
|
+
server_path = str(server_path or '').strip()
|
|
196
|
+
wait_for_selector = str(wait_for_selector or '').strip()
|
|
197
|
+
if not server_path and not wait_for_selector:
|
|
198
|
+
return {'status': 'skipped_missing_inputs'}
|
|
199
|
+
|
|
200
|
+
payload, cache_path = load_capture_hint_cache(state)
|
|
201
|
+
hints = payload.get('hints') or []
|
|
202
|
+
request_tokens = request_shape_tokens(state)
|
|
203
|
+
entry = {
|
|
204
|
+
'saved_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
205
|
+
'verification_mode': str(state.get('verification_mode') or '').strip().lower(),
|
|
206
|
+
'request_tokens': request_tokens,
|
|
207
|
+
'request_sample': compact_debug_value(str(state.get('change_request') or '')[:240]),
|
|
208
|
+
'server_path': server_path,
|
|
209
|
+
'wait_for_selector': wait_for_selector,
|
|
210
|
+
'observed_path': str(observed_path or '').strip(),
|
|
211
|
+
'proof_profile_name': str(((state.get('proof_profile') or {}).get('name')) or '').strip(),
|
|
212
|
+
'source_stage': str(source_stage or '').strip(),
|
|
213
|
+
'success_signal': str(success_signal or '').strip(),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
deduped = []
|
|
217
|
+
for hint in hints:
|
|
218
|
+
if not isinstance(hint, dict):
|
|
219
|
+
continue
|
|
220
|
+
if (
|
|
221
|
+
str(hint.get('verification_mode') or '').strip().lower() == entry['verification_mode']
|
|
222
|
+
and str(hint.get('server_path') or '').strip() == entry['server_path']
|
|
223
|
+
and str(hint.get('wait_for_selector') or '').strip() == entry['wait_for_selector']
|
|
224
|
+
and list(hint.get('request_tokens') or []) == entry['request_tokens']
|
|
225
|
+
):
|
|
226
|
+
continue
|
|
227
|
+
deduped.append(hint)
|
|
228
|
+
|
|
229
|
+
next_payload = {
|
|
230
|
+
'version': CAPTURE_HINT_CACHE_VERSION,
|
|
231
|
+
'repo': str(state.get('repo') or state.get('repo_dir') or '').strip(),
|
|
232
|
+
'updated_at': entry['saved_at'],
|
|
233
|
+
'hints': [entry] + deduped[:CAPTURE_HINT_CACHE_LIMIT - 1],
|
|
234
|
+
}
|
|
235
|
+
try:
|
|
236
|
+
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
|
237
|
+
with open(cache_path, 'w') as f:
|
|
238
|
+
json.dump(next_payload, f, indent=2)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
return {
|
|
241
|
+
'status': 'error',
|
|
242
|
+
'cache_path': cache_path,
|
|
243
|
+
'error': str(exc)[:240],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
'status': 'saved',
|
|
248
|
+
'cache_path': cache_path,
|
|
249
|
+
'entry': entry,
|
|
250
|
+
'hint_count': len(next_payload['hints']),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
44
254
|
def compact_debug_value(value, limit=DEBUG_STRING_LIMIT):
|
|
45
255
|
if isinstance(value, str) and len(value) > limit:
|
|
46
256
|
return value[:limit] + '... [truncated]'
|
package/runtime/lib/verify.py
CHANGED
|
@@ -19,6 +19,7 @@ from util import (
|
|
|
19
19
|
invoke_retry,
|
|
20
20
|
load_state,
|
|
21
21
|
prepare_server_preview,
|
|
22
|
+
record_successful_capture_hint,
|
|
22
23
|
save_state,
|
|
23
24
|
should_use_static_preview,
|
|
24
25
|
summarize_capture_artifacts,
|
|
@@ -1139,6 +1140,16 @@ if proof_evidence_required_for_mode(s.get('verification_mode')):
|
|
|
1139
1140
|
|
|
1140
1141
|
has_good_evidence = required_baseline_present and after_observation.get('valid') and not proof_evidence_blocker
|
|
1141
1142
|
|
|
1143
|
+
if has_good_evidence:
|
|
1144
|
+
s['capture_hint_saved'] = record_successful_capture_hint(
|
|
1145
|
+
s,
|
|
1146
|
+
server_path=expected_path or s.get('server_path') or '/',
|
|
1147
|
+
wait_for_selector=s.get('wait_for_selector') or '',
|
|
1148
|
+
observed_path=observed_path,
|
|
1149
|
+
source_stage='verify',
|
|
1150
|
+
success_signal='evidence_captured',
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1142
1153
|
if has_good_evidence:
|
|
1143
1154
|
supervisor_request = build_supervisor_assessment_request(s, after_payload, after_observation, required_baseline_present, expected_path, evidence_bundle)
|
|
1144
1155
|
s['verify_status'] = 'evidence_captured'
|