@riddledc/riddle-proof 0.5.11 → 0.5.12

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -170,6 +170,113 @@ def resolve_worktree_root(repo_dir):
170
170
  return os.path.join(tempfile.gettempdir(), '.riddle-proof-worktrees')
171
171
 
172
172
 
173
+ def env_flag(name, default=False):
174
+ raw = os.environ.get(name, '').strip().lower()
175
+ if raw in ('1', 'true', 'yes', 'on'):
176
+ return True
177
+ if raw in ('0', 'false', 'no', 'off'):
178
+ return False
179
+ return default
180
+
181
+
182
+ def env_int(name, default):
183
+ raw = os.environ.get(name, '').strip()
184
+ try:
185
+ value = int(raw)
186
+ except Exception:
187
+ return default
188
+ return value if value > 0 else default
189
+
190
+
191
+ def disk_free_bytes(path):
192
+ probe = path
193
+ while probe and not os.path.exists(probe):
194
+ parent = os.path.dirname(probe)
195
+ if parent == probe:
196
+ break
197
+ probe = parent
198
+ try:
199
+ return shutil.disk_usage(probe or tempfile.gettempdir()).free
200
+ except Exception:
201
+ return 0
202
+
203
+
204
+ def prune_scratch_worktrees(worktree_root, keep_dirs, repo_dir):
205
+ report = {
206
+ 'requested': True,
207
+ 'worktree_root': worktree_root,
208
+ 'removed': [],
209
+ 'errors': [],
210
+ }
211
+ if env_flag('RIDDLE_PROOF_KEEP_SCRATCH_WORKTREES', False):
212
+ report['skipped'] = 'RIDDLE_PROOF_KEEP_SCRATCH_WORKTREES'
213
+ return report
214
+ if not worktree_root:
215
+ report['skipped'] = 'missing_worktree_root'
216
+ return report
217
+
218
+ root = os.path.abspath(os.path.expanduser(worktree_root))
219
+ temp_root = os.path.abspath(tempfile.gettempdir())
220
+ if root in ('/', temp_root) or not root.endswith('.riddle-proof-worktrees'):
221
+ report['skipped'] = 'unsafe_worktree_root'
222
+ return report
223
+ if not os.path.isdir(root):
224
+ report['skipped'] = 'worktree_root_missing'
225
+ return report
226
+
227
+ min_free_bytes = env_int('RIDDLE_PROOF_MIN_SCRATCH_FREE_MB', 2048) * 1024 * 1024
228
+ free_before = disk_free_bytes(root)
229
+ report['free_before_bytes'] = free_before
230
+ report['min_free_bytes'] = min_free_bytes
231
+ if free_before >= min_free_bytes:
232
+ report['skipped'] = 'enough_free_space'
233
+ return report
234
+
235
+ keep = set(os.path.abspath(os.path.expanduser(p)) for p in keep_dirs if p)
236
+ candidates = []
237
+ for name in os.listdir(root):
238
+ if not name.startswith('riddle-proof-'):
239
+ continue
240
+ path = os.path.join(root, name)
241
+ resolved = os.path.abspath(path)
242
+ if resolved in keep or not os.path.isdir(path):
243
+ continue
244
+ try:
245
+ mtime = os.path.getmtime(path)
246
+ except Exception:
247
+ mtime = 0
248
+ candidates.append((mtime, path))
249
+ candidates.sort()
250
+
251
+ for _, path in candidates:
252
+ if disk_free_bytes(root) >= min_free_bytes:
253
+ break
254
+ removed_by_git = False
255
+ git_error = ''
256
+ if repo_dir and os.path.exists(os.path.join(repo_dir, '.git')):
257
+ remove_result = sp.run(
258
+ 'git worktree remove --force ' + shell_quote(path),
259
+ shell=True,
260
+ cwd=repo_dir,
261
+ capture_output=True,
262
+ text=True,
263
+ )
264
+ removed_by_git = remove_result.returncode == 0
265
+ if remove_result.returncode != 0 and os.path.exists(path):
266
+ git_error = (remove_result.stderr or remove_result.stdout or '')[:300]
267
+ if os.path.exists(path):
268
+ shutil.rmtree(path, ignore_errors=True)
269
+ if not os.path.exists(path):
270
+ report['removed'].append({'path': path, 'via': 'git' if removed_by_git else 'filesystem'})
271
+ elif git_error:
272
+ report['errors'].append({'path': path, 'git_error': git_error})
273
+
274
+ if repo_dir and os.path.exists(os.path.join(repo_dir, '.git')):
275
+ git('git worktree prune', repo_dir)
276
+ report['free_after_bytes'] = disk_free_bytes(root)
277
+ return report
278
+
279
+
173
280
  def cleanup_legacy_branch_worktrees(repo_dir, branch_name):
174
281
  if not repo_dir or not os.path.exists(os.path.join(repo_dir, '.git')):
175
282
  return
@@ -370,6 +477,11 @@ s['worktree_root'] = WORKTREE_ROOT
370
477
  save_state(s)
371
478
  print('Prepared workspace via ' + setup.get('source', 'workspace_core') + ': ' + repo_dir)
372
479
  os.makedirs(WORKTREE_ROOT, exist_ok=True)
480
+ scratch_cleanup = prune_scratch_worktrees(WORKTREE_ROOT, (BEFORE_DIR, AFTER_DIR), repo_dir)
481
+ if scratch_cleanup.get('removed') or scratch_cleanup.get('errors'):
482
+ print('Scratch cleanup: removed ' + str(len(scratch_cleanup.get('removed') or [])) + ' stale proof worktree(s)')
483
+ s['scratch_cleanup'] = scratch_cleanup
484
+ save_state(s)
373
485
  cleanup_legacy_branch_worktrees(repo_dir, base_branch)
374
486
 
375
487
  # Clean any stale worktrees for this run and the legacy fixed paths