@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,151 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const TOOL_DEFAULT_INCLUDE = ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"];
8
+
9
+ function readJson(path) {
10
+ try {
11
+ return JSON.parse(readFileSync(path, "utf8"));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function readOpenClawPluginConfig() {
18
+ const paths = [
19
+ process.env.OPENCLAW_CONFIG,
20
+ process.env.OPENCLAW_HOME ? join(process.env.OPENCLAW_HOME, "openclaw.json") : "",
21
+ join(homedir(), ".openclaw", "openclaw.json"),
22
+ "/root/.openclaw/openclaw.json",
23
+ ].filter(Boolean);
24
+
25
+ for (const path of paths) {
26
+ const cfg = readJson(path);
27
+ const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config;
28
+ if (pluginCfg) return pluginCfg;
29
+ }
30
+ return {};
31
+ }
32
+
33
+ function buildConfig() {
34
+ const pluginCfg = readOpenClawPluginConfig();
35
+ return {
36
+ apiKey: process.env.RIDDLE_API_KEY || pluginCfg.apiKey,
37
+ baseUrl: pluginCfg.baseUrl || "https://api.riddledc.com",
38
+ workspace: process.env.OPENCLAW_WORKSPACE || process.cwd(),
39
+ };
40
+ }
41
+
42
+ async function importFileIfExists(path) {
43
+ if (!path || !existsSync(path)) return null;
44
+ return import(pathToFileURL(path).href);
45
+ }
46
+
47
+ async function loadCore() {
48
+ try {
49
+ return await import("@riddledc/openclaw-riddledc/core");
50
+ } catch (primaryErr) {
51
+ const candidates = [
52
+ process.env.RIDDLE_OPENCLAW_CORE_PATH,
53
+ "/root/.openclaw/extensions/openclaw-riddledc/dist/core.js",
54
+ "/root/.openclaw/extensions/@riddledc/openclaw-riddledc/dist/core.js",
55
+ "/root/.openclaw/extensions/node_modules/@riddledc/openclaw-riddledc/dist/core.js",
56
+ "/usr/lib/node_modules/@riddledc/openclaw-riddledc/dist/core.js",
57
+ ];
58
+
59
+ for (const candidate of candidates) {
60
+ const mod = await importFileIfExists(candidate);
61
+ if (mod) return mod;
62
+ }
63
+
64
+ const err = primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
65
+ throw new Error(
66
+ "Riddle core package not found. Install/upgrade @riddledc/openclaw-riddledc with the ./core export before running riddle-proof direct mode. Import error: " + err
67
+ );
68
+ }
69
+ }
70
+
71
+ function numberValue(value, fallback) {
72
+ const n = Number(value);
73
+ return Number.isFinite(n) ? n : fallback;
74
+ }
75
+
76
+ function normalizeServerArgs(args) {
77
+ return {
78
+ ...args,
79
+ directory: args.directory ?? args.dir,
80
+ image: args.image ?? args.server_image,
81
+ command: args.command ?? args.server_command,
82
+ port: numberValue(args.port ?? args.server_port, 3000),
83
+ path: args.path ?? args.server_path,
84
+ };
85
+ }
86
+
87
+ function normalizeBuildArgs(args) {
88
+ return {
89
+ ...args,
90
+ directory: args.directory ?? args.dir,
91
+ command: args.command ?? args.server_command,
92
+ port: numberValue(args.port ?? args.server_port, 3000),
93
+ path: args.path ?? args.server_path,
94
+ };
95
+ }
96
+
97
+ async function run(tool, args) {
98
+ const core = await loadCore();
99
+ const config = buildConfig();
100
+
101
+ if (tool === "riddle_preview") {
102
+ return core.createStaticPreview(config, {
103
+ directory: args.directory ?? args.dir,
104
+ framework: args.framework,
105
+ });
106
+ }
107
+
108
+ if (tool === "riddle_preview_delete") {
109
+ return core.deleteStaticPreview(config, args.id);
110
+ }
111
+
112
+ if (tool === "riddle_server_preview") {
113
+ return core.createServerPreview(config, normalizeServerArgs(args));
114
+ }
115
+
116
+ if (tool === "riddle_build_preview") {
117
+ return core.createBuildPreview(config, normalizeBuildArgs(args));
118
+ }
119
+
120
+ if (tool === "riddle_script") {
121
+ return core.runWithDefaults(config, args, {
122
+ include: TOOL_DEFAULT_INCLUDE,
123
+ returnAsync: !!args.async,
124
+ });
125
+ }
126
+
127
+ if (tool === "riddle_run") {
128
+ return core.runWithDefaults(config, args.payload ?? args, {
129
+ include: ["screenshot", "console", "result"],
130
+ returnAsync: !!args.async,
131
+ });
132
+ }
133
+
134
+ throw new Error("Unsupported direct Riddle tool: " + tool);
135
+ }
136
+
137
+ async function main() {
138
+ const tool = process.argv[2];
139
+ const rawArgs = process.argv[3] || "{}";
140
+ if (!tool) throw new Error("Usage: riddle_core_call.mjs <tool> <json-args>");
141
+ const args = JSON.parse(rawArgs);
142
+ const result = await run(tool, args);
143
+ console.log(JSON.stringify(result));
144
+ }
145
+
146
+ main().catch((err) => {
147
+ console.log(JSON.stringify({
148
+ ok: false,
149
+ error: err instanceof Error ? err.message : String(err),
150
+ }));
151
+ });
@@ -0,0 +1,387 @@
1
+ """Setup: create worktrees, install deps, validate args, write state.
2
+
3
+ Idempotent — safe to re-run. Creates per-run worktrees under the active
4
+ workspace root by default:
5
+ <workspace>/.riddle-proof-worktrees/riddle-proof-<run_id>-before
6
+ <workspace>/.riddle-proof-worktrees/riddle-proof-<run_id>-after
7
+ """
8
+
9
+ import json, subprocess as sp, os, sys, shutil
10
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
+ from util import load_state, save_state, git, shell_quote
12
+
13
+ s = load_state()
14
+ repo = s['repo']
15
+ branch = (s.get('target_branch') or s['branch']).strip()
16
+ repo_dir = s['repo_dir']
17
+ base_branch = s.get('base_branch', 'main')
18
+ before_ref_arg = (s.get('before_ref') or s.get('base_ref') or '').strip()
19
+ mode = s.get('mode', 'server')
20
+ reference = s.get('reference', 'both') # prod, before, both
21
+ run_id = (s.get('run_id') or '').strip()
22
+ SAFE_RUN_ID = ''.join(ch if ch.isalnum() or ch in ('-', '_') else '-' for ch in run_id) or 'run'
23
+
24
+ AFTER_WORKTREE_BRANCH = 'riddle-proof/' + SAFE_RUN_ID + '-after'
25
+ LEGACY_WORKTREE_DIRS = ('/tmp/riddle-proof-before', '/tmp/riddle-proof-after')
26
+
27
+ if branch.startswith('riddle-proof/'):
28
+ raise SystemExit(
29
+ 'Setup invariant failed: target_branch uses reserved riddle-proof/* namespace. '
30
+ 'Run preflight again so it can choose a real agent/openclaw/* PR branch.'
31
+ )
32
+
33
+
34
+ # In the packaged runtime, the shared workspace helper lives at package-root/lib
35
+ # while the stage scripts live under package-root/runtime.
36
+ SKILLS_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
37
+ WORKSPACE_CORE = os.path.join(SKILLS_ROOT, 'lib', 'workspace-core.mjs')
38
+
39
+
40
+ def workspace_core(command, payload, timeout=180):
41
+ if not os.path.exists(WORKSPACE_CORE):
42
+ raise SystemExit('workspace core helper missing: ' + WORKSPACE_CORE)
43
+ try:
44
+ result = sp.run(
45
+ ['node', WORKSPACE_CORE, command, json.dumps(payload)],
46
+ capture_output=True,
47
+ text=True,
48
+ timeout=timeout,
49
+ )
50
+ except sp.TimeoutExpired:
51
+ raise SystemExit('workspace core timed out for ' + command)
52
+
53
+ if result.returncode != 0:
54
+ detail = (result.stderr or result.stdout or '').strip()
55
+ raise SystemExit('workspace core failed for ' + command + ': ' + detail[:300])
56
+
57
+ try:
58
+ return json.loads(result.stdout)
59
+ except Exception:
60
+ raise SystemExit('workspace core returned invalid JSON for ' + command + ': ' + result.stdout[:300])
61
+
62
+
63
+ def ensure_deps(project_dir, reuse_from=''):
64
+ payload = {'projectDir': project_dir}
65
+ if reuse_from:
66
+ payload['reuseFrom'] = reuse_from
67
+ result = workspace_core('ensure-deps', payload, timeout=300)
68
+ return result.get('status', '')
69
+
70
+
71
+ def resolve_worktree_root(repo_dir):
72
+ configured = (s.get('worktree_root') or os.environ.get('RIDDLE_PROOF_WORKTREE_ROOT') or '').strip()
73
+ if configured:
74
+ return os.path.abspath(os.path.expanduser(configured))
75
+ repo_parent = os.path.dirname(os.path.abspath(repo_dir))
76
+ return os.path.join(repo_parent, '.riddle-proof-worktrees')
77
+
78
+
79
+ def cleanup_legacy_branch_worktrees(repo_dir, branch_name):
80
+ if not repo_dir or not os.path.exists(os.path.join(repo_dir, '.git')):
81
+ return
82
+ result = git('git worktree list --porcelain', repo_dir)
83
+ if result.returncode != 0:
84
+ return
85
+
86
+ worktrees = []
87
+ current = {}
88
+ for line in result.stdout.splitlines() + ['']:
89
+ if not line.strip():
90
+ if current:
91
+ worktrees.append(current)
92
+ current = {}
93
+ continue
94
+ key, _, value = line.partition(' ')
95
+ if key == 'worktree':
96
+ current['path'] = value.strip()
97
+ elif key == 'branch':
98
+ current['branch'] = value.strip()
99
+
100
+ locked_ref = 'refs/heads/' + branch_name
101
+ for wt in worktrees:
102
+ path = wt.get('path', '')
103
+ if not (path.startswith('/tmp/riddle-proof-') and path.endswith('-before')):
104
+ continue
105
+ if wt.get('branch') != locked_ref:
106
+ continue
107
+ print('Removing stale legacy riddle-proof worktree locked to ' + branch_name + ': ' + path)
108
+ sp.run(
109
+ 'git worktree remove --force ' + shell_quote(path),
110
+ shell=True,
111
+ cwd=repo_dir,
112
+ capture_output=True,
113
+ )
114
+ if os.path.exists(path):
115
+ shutil.rmtree(path, ignore_errors=True)
116
+ git('git worktree prune', repo_dir)
117
+
118
+
119
+ def ref_exists(repo_dir, ref):
120
+ if not ref:
121
+ return False
122
+ r = git('git rev-parse --verify --quiet ' + shell_quote(ref + '^{commit}'), repo_dir)
123
+ return r.returncode == 0
124
+
125
+
126
+ def resolve_before_ref(repo_dir, base_branch, requested_ref):
127
+ candidates = []
128
+ if requested_ref:
129
+ candidates.append((requested_ref, 'requested'))
130
+ if base_branch:
131
+ candidates.append(('origin/' + base_branch, 'remote_base_branch'))
132
+ candidates.append((base_branch, 'local_base_branch_fallback'))
133
+
134
+ for ref, source in candidates:
135
+ if ref_exists(repo_dir, ref):
136
+ return ref, source
137
+ raise SystemExit(
138
+ 'Failed to resolve before ref. Tried: ' +
139
+ ', '.join(ref for ref, _ in candidates if ref)
140
+ )
141
+
142
+
143
+ def load_repo_profile(project_dir):
144
+ profile_path = os.path.join(project_dir, '.riddle-proof', 'profile.json')
145
+ if not os.path.exists(profile_path):
146
+ return {}, ''
147
+ try:
148
+ with open(profile_path) as f:
149
+ profile = json.load(f)
150
+ except Exception as exc:
151
+ print('Ignoring invalid Riddle Proof profile: ' + profile_path + ' (' + str(exc)[:180] + ')')
152
+ return {}, profile_path
153
+ if not isinstance(profile, dict):
154
+ print('Ignoring non-object Riddle Proof profile: ' + profile_path)
155
+ return {}, profile_path
156
+ return profile, profile_path
157
+
158
+
159
+ def profile_matches_target(target, haystack):
160
+ keywords = target.get('keywords') if isinstance(target, dict) else []
161
+ if isinstance(keywords, str):
162
+ keywords = [keywords]
163
+ matched = []
164
+ for keyword in keywords if isinstance(keywords, list) else []:
165
+ needle = str(keyword).strip().lower()
166
+ if needle and needle in haystack:
167
+ matched.append(str(keyword).strip())
168
+ name = str(target.get('name') or '').strip()
169
+ if name and name.lower() in haystack and name not in matched:
170
+ matched.append(name)
171
+ return matched
172
+
173
+
174
+ def apply_repo_profile(project_dir):
175
+ profile, profile_path = load_repo_profile(project_dir)
176
+ if not profile:
177
+ return
178
+
179
+ haystack = ' '.join([
180
+ str(s.get('change_request') or ''),
181
+ str(s.get('context') or ''),
182
+ str(s.get('server_path') or ''),
183
+ str(s.get('capture_script') or ''),
184
+ ]).lower()
185
+
186
+ selected = {}
187
+ matched_keywords = []
188
+ targets = profile.get('targets')
189
+ if isinstance(targets, list):
190
+ for target in targets:
191
+ if not isinstance(target, dict):
192
+ continue
193
+ matched = profile_matches_target(target, haystack)
194
+ if matched:
195
+ selected = target
196
+ matched_keywords = matched
197
+ break
198
+
199
+ defaults = profile.get('defaults') if isinstance(profile.get('defaults'), dict) else {}
200
+ merged = dict(defaults)
201
+ if selected:
202
+ merged.update(selected)
203
+ if not merged:
204
+ return
205
+
206
+ applied = []
207
+ simple_fields = [
208
+ 'mode',
209
+ 'build_command',
210
+ 'build_output',
211
+ 'server_image',
212
+ 'server_command',
213
+ 'server_port',
214
+ 'server_path',
215
+ 'wait_for_selector',
216
+ 'color_scheme',
217
+ 'allow_static_preview_fallback',
218
+ 'use_auth',
219
+ 'success_criteria',
220
+ 'capture_script',
221
+ ]
222
+ for field in simple_fields:
223
+ value = merged.get(field)
224
+ if value is None or str(value).strip() == '':
225
+ continue
226
+ if str(s.get(field) or '').strip():
227
+ continue
228
+ s[field] = str(value).strip()
229
+ applied.append(field)
230
+ if field == 'server_path':
231
+ s['server_path_source'] = 'repo_profile'
232
+
233
+ profile_context = str(merged.get('context') or merged.get('proof_context') or '').strip()
234
+ if profile_context:
235
+ existing = str(s.get('context') or '').strip()
236
+ note = 'Riddle Proof repo profile'
237
+ target_name = str(merged.get('name') or '').strip()
238
+ if target_name:
239
+ note += ' (' + target_name + ')'
240
+ note += ': ' + profile_context
241
+ s['context'] = (existing + '\n\n' + note).strip() if existing else note
242
+ applied.append('context')
243
+
244
+ if applied:
245
+ s['proof_profile'] = {
246
+ 'path': profile_path,
247
+ 'name': str(merged.get('name') or '').strip(),
248
+ 'matched_keywords': matched_keywords,
249
+ 'applied_fields': sorted(set(applied)),
250
+ }
251
+ print('Applied Riddle Proof repo profile: ' + ', '.join(s['proof_profile']['applied_fields']))
252
+
253
+ # Ensure the repo is cloned and up to date via the shared workspace core.
254
+ setup = workspace_core('prepare-repo', {
255
+ 'repo': repo,
256
+ 'branch': branch,
257
+ 'repoDir': repo_dir,
258
+ 'baseBranch': base_branch,
259
+ 'workspaceRoot': os.environ.get('OPENCLAW_WORKSPACE', ''),
260
+ }, timeout=300)
261
+ repo_dir = setup.get('repoDir') or repo_dir
262
+ branch = setup.get('branch') or branch
263
+ target_branch = branch
264
+ if target_branch.startswith('riddle-proof/'):
265
+ raise SystemExit(
266
+ 'Setup invariant failed: workspace prepared a reserved riddle-proof/* branch instead of a real PR branch.'
267
+ )
268
+ WORKTREE_ROOT = resolve_worktree_root(repo_dir)
269
+ BEFORE_DIR = os.path.join(WORKTREE_ROOT, 'riddle-proof-' + SAFE_RUN_ID + '-before')
270
+ AFTER_DIR = os.path.join(WORKTREE_ROOT, 'riddle-proof-' + SAFE_RUN_ID + '-after')
271
+ s['repo_dir'] = repo_dir
272
+ s['branch'] = branch
273
+ s['target_branch'] = target_branch
274
+ s['ship_target_branch'] = target_branch
275
+ s['worktree_root'] = WORKTREE_ROOT
276
+ save_state(s)
277
+ print('Prepared workspace via ' + setup.get('source', 'workspace_core') + ': ' + repo_dir)
278
+ os.makedirs(WORKTREE_ROOT, exist_ok=True)
279
+ cleanup_legacy_branch_worktrees(repo_dir, base_branch)
280
+
281
+ # Clean any stale worktrees for this run and the legacy fixed paths
282
+ worktree_cleanup_dirs = []
283
+ for candidate in (
284
+ BEFORE_DIR,
285
+ AFTER_DIR,
286
+ s.get('before_worktree'),
287
+ s.get('after_worktree'),
288
+ *LEGACY_WORKTREE_DIRS,
289
+ ):
290
+ if candidate and candidate not in worktree_cleanup_dirs:
291
+ worktree_cleanup_dirs.append(candidate)
292
+
293
+ cleanup_branches = []
294
+ for candidate in (AFTER_WORKTREE_BRANCH, s.get('after_worktree_branch', '').strip()):
295
+ if candidate and candidate not in cleanup_branches:
296
+ cleanup_branches.append(candidate)
297
+
298
+ # Create before worktree (only if reference includes 'before')
299
+ before_ref = ''
300
+ before_ref_source = ''
301
+ if reference in ('before', 'both'):
302
+ before_ref, before_ref_source = resolve_before_ref(repo_dir, base_branch, before_ref_arg)
303
+ workspace_core('ensure-worktree', {
304
+ 'repoDir': repo_dir,
305
+ 'worktreeDir': BEFORE_DIR,
306
+ 'ref': before_ref,
307
+ 'detach': True,
308
+ 'cleanupPaths': worktree_cleanup_dirs,
309
+ 'verifyPackageJson': True,
310
+ }, timeout=300)
311
+ print('Before worktree: ' + BEFORE_DIR + ' (' + before_ref + ', source=' + before_ref_source + ')')
312
+
313
+ # Patch Next.js config if needed (export -> standalone for server mode)
314
+ if mode == 'server':
315
+ for cf in (BEFORE_DIR + '/next.config.ts', BEFORE_DIR + '/next.config.js', BEFORE_DIR + '/next.config.mjs'):
316
+ if os.path.exists(cf):
317
+ with open(cf) as f:
318
+ content = f.read()
319
+ if "output: 'export'" in content:
320
+ with open(cf, 'w') as f:
321
+ f.write(content.replace("output: 'export'", "output: 'standalone'"))
322
+ print('Patched before config: export -> standalone')
323
+ break
324
+
325
+ # Create after worktree
326
+ after_cleanup_dirs = [candidate for candidate in worktree_cleanup_dirs if candidate != BEFORE_DIR]
327
+ workspace_core('ensure-worktree', {
328
+ 'repoDir': repo_dir,
329
+ 'worktreeDir': AFTER_DIR,
330
+ 'ref': branch,
331
+ 'branchName': AFTER_WORKTREE_BRANCH,
332
+ 'resetBranch': True,
333
+ 'cleanupPaths': after_cleanup_dirs,
334
+ 'cleanupBranches': cleanup_branches,
335
+ 'verifyPackageJson': True,
336
+ }, timeout=300)
337
+ print('After worktree: ' + AFTER_DIR + ' (' + AFTER_WORKTREE_BRANCH + ' -> ' + branch + ')')
338
+ apply_repo_profile(AFTER_DIR)
339
+ save_state(s)
340
+
341
+ reuse_source = repo_dir if os.path.exists(os.path.join(repo_dir, 'package.json')) else ''
342
+ shared_status = ensure_deps(reuse_source) if reuse_source else ''
343
+ if shared_status:
344
+ print('Shared deps status: ' + shared_status)
345
+
346
+ before_dep_status = ''
347
+ if reference in ('before', 'both'):
348
+ before_dep_status = ensure_deps(BEFORE_DIR, reuse_from=reuse_source)
349
+ print('Before deps status: ' + before_dep_status)
350
+
351
+ after_dep_status = ensure_deps(AFTER_DIR, reuse_from=reuse_source)
352
+ print('After deps status: ' + after_dep_status)
353
+
354
+ # Patch Next.js config in after worktree if needed
355
+ if mode == 'server':
356
+ for cf in (AFTER_DIR + '/next.config.ts', AFTER_DIR + '/next.config.js', AFTER_DIR + '/next.config.mjs'):
357
+ if os.path.exists(cf):
358
+ with open(cf) as f:
359
+ content = f.read()
360
+ if "output: 'export'" in content:
361
+ with open(cf, 'w') as f:
362
+ f.write(content.replace("output: 'export'", "output: 'standalone'"))
363
+ print('Patched after config: export -> standalone')
364
+ break
365
+
366
+ s['before_worktree'] = BEFORE_DIR if reference in ('before', 'both') else ''
367
+ s['before_ref'] = before_ref or before_ref_arg
368
+ s['before_ref_source'] = before_ref_source
369
+ s['after_worktree'] = AFTER_DIR
370
+ s['after_worktree_branch'] = AFTER_WORKTREE_BRANCH
371
+ s['workspace_ready'] = True
372
+ s['stage'] = 'setup'
373
+ s['implementation_status'] = 'pending_recon'
374
+ s['dependency_install'] = {
375
+ 'shared': bool(shared_status),
376
+ 'before': before_dep_status,
377
+ 'after': after_dep_status,
378
+ }
379
+ if not (s.get('capture_script') or '').strip():
380
+ s['proof_plan_status'] = 'pending_recon'
381
+ save_state(s)
382
+
383
+ print('Setup complete.')
384
+ if reference in ('before', 'both'):
385
+ print(' Before: ' + BEFORE_DIR + ' (detached ' + (before_ref or before_ref_arg) + ')')
386
+ print(' After: ' + AFTER_DIR + ' (' + AFTER_WORKTREE_BRANCH + ' -> ' + branch + ')')
387
+ print(json.dumps({'ok': True}))