kylebrodeur commited on
Commit
0b338fe
Β·
verified Β·
1 Parent(s): a2fa1e1

deploy: update Space from deploy_preflight --push

Browse files
.codeboarding/logs/wrapper-server.log CHANGED
@@ -348,3 +348,144 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
348
  [stderr] 2026-06-14 08:27:24 INFO [watchfiles.main:308] 1 change detected
349
  [stderr] 2026-06-14 08:27:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
350
  [stderr] 2026-06-14 08:27:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  [stderr] 2026-06-14 08:27:24 INFO [watchfiles.main:308] 1 change detected
349
  [stderr] 2026-06-14 08:27:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
350
  [stderr] 2026-06-14 08:27:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
351
+ [stderr] 2026-06-14 08:38:21 INFO [watchfiles.main:308] 1 change detected
352
+ [stderr] 2026-06-14 08:38:21 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
353
+ [stderr] 2026-06-14 08:38:21 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
354
+ [stderr] 2026-06-14 08:38:51 INFO [watchfiles.main:308] 1 change detected
355
+ [stderr] 2026-06-14 08:38:52 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
356
+ [stderr] 2026-06-14 08:38:52 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
357
+ [stderr] 2026-06-14 08:39:24 INFO [watchfiles.main:308] 1 change detected
358
+ [stderr] 2026-06-14 08:39:25 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
359
+ [stderr] 2026-06-14 08:39:25 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
360
+ [stderr] 2026-06-14 08:39:37 INFO [watchfiles.main:308] 1 change detected
361
+ [stderr] 2026-06-14 08:39:37 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
362
+ [stderr] 2026-06-14 08:39:37 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
363
+ [stderr] 2026-06-14 08:39:43 INFO [watchfiles.main:308] 1 change detected
364
+ [stderr] 2026-06-14 08:39:44 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
365
+ [stderr] 2026-06-14 08:39:44 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
366
+ [stderr] 2026-06-14 08:40:08 INFO [watchfiles.main:308] 1 change detected
367
+ [stderr] 2026-06-14 08:40:08 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
368
+ [stderr] 2026-06-14 08:40:08 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
369
+ [stderr] 2026-06-14 08:42:15 INFO [watchfiles.main:308] 1 change detected
370
+ [stderr] 2026-06-14 08:42:15 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
371
+ [stderr] 2026-06-14 08:42:15 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
372
+ [stderr] 2026-06-14 08:42:17 INFO [watchfiles.main:308] 1 change detected
373
+ [stderr] 2026-06-14 08:42:17 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
374
+ [stderr] 2026-06-14 08:42:17 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
375
+ [stderr] 2026-06-14 08:42:27 INFO [watchfiles.main:308] 1 change detected
376
+ [stderr] 2026-06-14 08:42:27 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
377
+ [stderr] 2026-06-14 08:42:27 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
378
+ [stderr] 2026-06-14 08:42:32 INFO [watchfiles.main:308] 1 change detected
379
+ [stderr] 2026-06-14 08:42:33 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
380
+ [stderr] 2026-06-14 08:42:33 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
381
+ [stderr] 2026-06-14 08:42:37 INFO [watchfiles.main:308] 1 change detected
382
+ [stderr] 2026-06-14 08:42:37 INFO [watchfiles.main:308] 1 change detected
383
+ [stderr] 2026-06-14 08:42:38 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
384
+ [stderr] 2026-06-14 08:42:38 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
385
+ [stderr] 2026-06-14 08:42:41 INFO [watchfiles.main:308] 1 change detected
386
+ [stderr] 2026-06-14 08:42:42 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
387
+ [stderr] 2026-06-14 08:42:42 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
388
+ [stderr] 2026-06-14 08:42:49 INFO [watchfiles.main:308] 1 change detected
389
+ [stderr] 2026-06-14 08:42:49 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
390
+ [stderr] 2026-06-14 08:42:49 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
391
+ [stderr] 2026-06-14 08:42:52 INFO [watchfiles.main:308] 1 change detected
392
+ [stderr] 2026-06-14 08:42:52 INFO [watchfiles.main:308] 1 change detected
393
+ [stderr] 2026-06-14 08:42:53 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
394
+ [stderr] 2026-06-14 08:42:53 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
395
+ [stderr] 2026-06-14 08:42:58 INFO [watchfiles.main:308] 1 change detected
396
+ [stderr] 2026-06-14 08:42:58 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
397
+ [stderr] 2026-06-14 08:42:58 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
398
+ [stderr] 2026-06-14 08:43:12 INFO [watchfiles.main:308] 1 change detected
399
+ [stderr] 2026-06-14 08:43:13 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
400
+ [stderr] 2026-06-14 08:43:13 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
401
+ [stderr] 2026-06-14 08:43:21 INFO [watchfiles.main:308] 1 change detected
402
+ [stderr] 2026-06-14 08:43:22 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
403
+ [stderr] 2026-06-14 08:43:22 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
404
+ [stderr] 2026-06-14 08:43:25 INFO [watchfiles.main:308] 1 change detected
405
+ [stderr] 2026-06-14 08:43:31 INFO [watchfiles.main:308] 1 change detected
406
+ [stderr] 2026-06-14 08:43:34 INFO [watchfiles.main:308] 1 change detected
407
+ [stderr] 2026-06-14 08:43:34 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
408
+ [stderr] 2026-06-14 08:43:34 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
409
+ [stderr] 2026-06-14 08:43:50 INFO [watchfiles.main:308] 1 change detected
410
+ [stderr] 2026-06-14 08:43:50 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
411
+ [stderr] 2026-06-14 08:43:50 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
412
+ [stderr] 2026-06-14 08:43:52 INFO [watchfiles.main:308] 1 change detected
413
+ [stderr] 2026-06-14 08:43:52 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
414
+ [stderr] 2026-06-14 08:43:52 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
415
+ [stderr] 2026-06-14 08:44:05 INFO [watchfiles.main:308] 1 change detected
416
+ [stderr] 2026-06-14 08:44:05 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
417
+ [stderr] 2026-06-14 08:44:05 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
418
+ [stderr] 2026-06-14 08:44:14 INFO [watchfiles.main:308] 1 change detected
419
+ [stderr] 2026-06-14 08:44:14 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
420
+ [stderr] 2026-06-14 08:44:14 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
421
+ [stderr] 2026-06-14 08:44:20 INFO [watchfiles.main:308] 1 change detected
422
+ [stderr] 2026-06-14 08:44:20 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
423
+ [stderr] 2026-06-14 08:44:20 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
424
+ [stderr] 2026-06-14 08:47:55 INFO [watchfiles.main:308] 2 changes detected
425
+ [stderr] 2026-06-14 08:47:55 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
426
+ [stderr] 2026-06-14 08:47:55 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
427
+ [stderr] 2026-06-14 08:47:59 INFO [watchfiles.main:308] 1 change detected
428
+ [stderr] 2026-06-14 08:48:00 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
429
+ [stderr] 2026-06-14 08:48:00 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
430
+ [stderr] 2026-06-14 08:48:08 INFO [watchfiles.main:308] 1 change detected
431
+ [stderr] 2026-06-14 08:48:09 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
432
+ [stderr] 2026-06-14 08:48:09 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
433
+ [stderr] 2026-06-14 08:48:13 INFO [watchfiles.main:308] 1 change detected
434
+ [stderr] 2026-06-14 08:48:14 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
435
+ [stderr] 2026-06-14 08:48:14 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
436
+ [stderr] 2026-06-14 08:48:25 INFO [watchfiles.main:308] 1 change detected
437
+ [stderr] 2026-06-14 08:48:25 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
438
+ [stderr] 2026-06-14 08:48:25 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
439
+ [stderr] 2026-06-14 08:48:31 INFO [watchfiles.main:308] 1 change detected
440
+ [stderr] 2026-06-14 08:48:31 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
441
+ [stderr] 2026-06-14 08:48:31 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
442
+ [stderr] 2026-06-14 08:48:36 INFO [watchfiles.main:308] 1 change detected
443
+ [stderr] 2026-06-14 08:48:37 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
444
+ [stderr] 2026-06-14 08:48:37 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
445
+ [stderr] 2026-06-14 08:48:40 INFO [watchfiles.main:308] 1 change detected
446
+ [stderr] 2026-06-14 08:48:41 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
447
+ [stderr] 2026-06-14 08:48:41 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
448
+ [stderr] 2026-06-14 08:48:43 INFO [watchfiles.main:308] 1 change detected
449
+ [stderr] 2026-06-14 08:48:43 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
450
+ [stderr] 2026-06-14 08:48:43 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
451
+ [stderr] 2026-06-14 08:48:46 INFO [watchfiles.main:308] 1 change detected
452
+ [stderr] 2026-06-14 08:48:46 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
453
+ [stderr] 2026-06-14 08:48:46 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
454
+ [stderr] 2026-06-14 08:48:48 INFO [watchfiles.main:308] 1 change detected
455
+ [stderr] 2026-06-14 08:48:48 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
456
+ [stderr] 2026-06-14 08:48:48 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
457
+ [stderr] 2026-06-14 08:48:50 INFO [watchfiles.main:308] 1 change detected
458
+ [stderr] 2026-06-14 08:48:50 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
459
+ [stderr] 2026-06-14 08:48:50 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
460
+ [stderr] 2026-06-14 08:48:55 INFO [watchfiles.main:308] 1 change detected
461
+ [stderr] 2026-06-14 08:48:56 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
462
+ [stderr] 2026-06-14 08:48:56 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
463
+ [stderr] 2026-06-14 08:49:02 INFO [watchfiles.main:308] 1 change detected
464
+ [stderr] 2026-06-14 08:49:50 INFO [watchfiles.main:308] 1 change detected
465
+ [stderr] 2026-06-14 08:54:18 INFO [watchfiles.main:308] 1 change detected
466
+ [stderr] 2026-06-14 08:54:19 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
467
+ [stderr] 2026-06-14 08:54:19 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
468
+ [stderr] 2026-06-14 08:54:40 INFO [watchfiles.main:308] 1 change detected
469
+ [stderr] 2026-06-14 08:54:40 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
470
+ [stderr] 2026-06-14 08:54:40 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
471
+ [stderr] 2026-06-14 08:54:45 INFO [watchfiles.main:308] 1 change detected
472
+ [stderr] 2026-06-14 08:54:45 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
473
+ [stderr] 2026-06-14 08:54:45 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
474
+ [stderr] 2026-06-14 08:55:09 INFO [watchfiles.main:308] 1 change detected
475
+ [stderr] 2026-06-14 08:55:10 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
476
+ [stderr] 2026-06-14 08:55:10 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
477
+ [stderr] 2026-06-14 08:55:29 INFO [watchfiles.main:308] 1 change detected
478
+ [stderr] 2026-06-14 08:55:29 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
479
+ [stderr] 2026-06-14 08:55:29 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
480
+ [stderr] 2026-06-14 08:55:56 INFO [watchfiles.main:308] 1 change detected
481
+ [stderr] 2026-06-14 08:55:57 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
482
+ [stderr] 2026-06-14 08:55:57 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
483
+ [stderr] 2026-06-14 08:56:52 INFO [watchfiles.main:308] 1 change detected
484
+ [stderr] 2026-06-14 08:56:53 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
485
+ [stderr] 2026-06-14 08:56:53 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
486
+ [stderr] 2026-06-14 08:57:00 INFO [watchfiles.main:308] 1 change detected
487
+ [stderr] 2026-06-14 08:57:00 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
488
+ [stderr] 2026-06-14 08:57:00 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
489
+ [stderr] 2026-06-14 08:57:08 INFO [watchfiles.main:308] 1 change detected
490
+ [stderr] 2026-06-14 08:57:09 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
491
+ [stderr] 2026-06-14 08:57:09 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
app.py CHANGED
@@ -18,7 +18,9 @@ from __future__ import annotations
18
 
19
  import os
20
  import random
 
21
  import uuid
 
22
 
23
  import gradio as gr
24
 
@@ -48,7 +50,7 @@ from core.models import Advice, BED_POSITIONS, Environment, Job, MATERIALS, Prin
48
  from core.nodes import render_node_cards
49
  from core.reflect import reflect_on_job
50
  from core.spine import SpineValidator
51
- from learn.loop import run_session
52
  from learn.policy import LearnedPolicy, cell_key
53
  from core.viewer import (
54
  GEO_READS,
@@ -174,7 +176,8 @@ def studio_log_html() -> str:
174
 
175
  def reset_learnings():
176
  """Reset the live ledger + learned policy to the curated baseline (seed + ingested),
177
- clearing this session's accumulated runs. Wired to the persistent Reset on every tab."""
 
178
  removed = LEDGER.reset_to_baseline()
179
  POLICY.reset()
180
  gr.Info(f"Reset to baseline β€” cleared {removed} runtime lesson(s) + the learned policy.")
@@ -188,6 +191,13 @@ def reset_learnings():
188
  gr.update(visible=False), # print results_group (re-hide)
189
  "", # real_log_msg
190
  studio_log_html(), # studio_log
 
 
 
 
 
 
 
191
  )
192
 
193
 
@@ -219,21 +229,31 @@ def _set_part(geometry: str, mesh, label: str, read: str | None = None):
219
 
220
 
221
  def build_start(part):
222
- """Instant feedback when BUILD is clicked: jump to Build, show ONE consolidated
223
- loader, hide the (stale) results until the model returns. No part β†’ stay put."""
 
224
  if not (part and part.get("geometry")):
225
  gr.Warning("Load a part on the BUILD tab first β€” quick-load Benchy, generate a primitive, or drop a mesh.")
226
- return (gr.update(),) * 9
 
 
 
 
227
  return (
228
  gr.Tabs(selected="build"), # tabs
229
- loader("SLICING THE PART Β· reading precedent, proposing settings, running checks"), # build_loader
230
- gr.update(visible=False), # build_results (hide until ready)
 
 
 
 
 
231
  gr.update(interactive=True), # to_print_btn (un-gate)
232
  gr.update(visible=False), # override_btn (hide)
233
- "", # second_opinion_panel (clear stale verdict)
234
  gr.update(value="Engineer's Read"), # read_toggle (reset to the read)
235
- gr.update(visible=True), # eng_read_group
236
- gr.update(visible=False), # second_op_group
237
  )
238
 
239
 
@@ -271,16 +291,30 @@ def _status_html() -> str:
271
 
272
 
273
  def warm_up_pending() -> str:
274
- return ("<div class='ce-sub' style='font-size:12px;'>MODEL Β· "
275
- "warming up (first load can take ~30s on ZeroGPU)…</div>")
 
276
 
277
 
278
  def warm_up_cb() -> str:
279
- return f"<div class='ce-sub' style='font-size:12px;'>MODEL Β· {llm.warm_up()}</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
 
282
  def _model_callout() -> str:
283
- """Hover callout next to the model switcher (replaces the full-width info block)."""
284
  return (
285
  "<span class='ce-callout' tabindex='0'>" + icon("info") + " MODEL INFO"
286
  "<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "
@@ -291,16 +325,14 @@ def _model_callout() -> str:
291
 
292
 
293
  def select_model(choice: str) -> str:
294
- """Model switcher (backend by learn/finetune via _apply_model_choice). Applies the
295
- chosen backend, then reports the model actually serving. The LoRA v2/v3 adapters serve
296
- through ZeroGPU on the Space (and fall back gracefully if the GPU/deps aren't present);
297
- Base is the stock Gemma 4 E4B; Modal API is the remote endpoint."""
298
  choice = choice or MODEL_OPTIONS[0]
299
  try:
300
  _apply_model_choice(choice)
301
  except Exception:
302
  pass
303
- return f"<div class='ce-sub' style='font-size:12px;'>MODEL Β· {choice} Β· {llm.backend_status()}</div>"
304
 
305
 
306
  # ── simulated environment (this is a sim lab β€” conditions are generated, overridable) ──
@@ -357,7 +389,8 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
357
  rec = advise(job, env, retrieved, references, policy_note)
358
  spine = SPINE.check(rec.advice.settings, material)
359
  hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
360
- vp_html = virtual_printer_html(mesh, f"{material} Β· {geometry_type}") # init on BUILD
 
361
 
362
  precedent = precedent_eval_html(retrieved, env)
363
  if retrieved:
@@ -374,9 +407,11 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
374
  confirm_vis = gr.update(visible=spine.requires_approval)
375
  approval_md = ("**HITL gate:** the Spine clamped a boundary setting β€” review, then **Confirm & Print**."
376
  if spine.requires_approval else "Within safe envelope β€” ready when you are.")
 
377
  session_id = uuid.uuid4().hex
378
  state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
379
- "advice": rec.advice.model_dump(), "label": part.get("label"), "session_id": session_id}
 
380
 
381
  # ── field log (Space only β€” gated on HF_TOKEN; local/offline no-ops) ──
382
  field_log.log_build(
@@ -401,22 +436,22 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
401
  f"**Chief Engineer O'Brien:** {rec.advice.reasoning}", # reasoning
402
  risk_callouts_html(rec.advice.risks, hint) + placement_callout(material, bed_position), # risks
403
  settings_panel_html(spine.settings, material), # settings (LCARS panel)
404
- f"{spine_md}\n\n{approval_md}" if spine_md else approval_md, # spine notes
405
  gcode_panel_html(spine.settings, material), # g-code (LCARS panel)
406
  confirm_vis, # confirm visibility
407
  render_node_cards(env, working=True), # node cards
408
  state, # state
409
- vp_html, # virtual print (animates once)
 
410
  gr.update(value=1), # reset layer scrubber
411
  layer_image(mesh, 1), # initial scrubbed layer
412
- "", # build_loader (clear)
413
- gr.update(visible=True), # build_results (reveal)
414
  )
415
 
416
 
417
- def second_opinion(state):
418
- """A SEPARATE Inspector persona critiques the plan before any print runs. A
419
- 'dispute' verdict GATES β†’ PRINT until acknowledged; caution/concur are advisory."""
420
  if not state or "advice" not in state:
421
  return ("<div class='ce-sub'>Build a job first β€” then I'll give the plan a second look.</div>",
422
  gr.update(interactive=True), gr.update(visible=False))
@@ -442,14 +477,42 @@ def second_opinion(state):
442
  return panel, gr.update(interactive=True), gr.update(visible=False)
443
 
444
 
 
 
 
 
 
 
 
 
 
 
 
445
  def toggle_read(choice, state):
446
  """Segmented toggle on Build: flip between Engineer's Read and Second Opinion,
447
- showing one panel at a time. The opinion is computed lazily on first reveal."""
 
 
448
  if str(choice).lower().startswith("engineer"):
449
- return (gr.update(visible=True), gr.update(visible=False),
450
- gr.update(), gr.update(), gr.update())
451
- panel, to_print, override = second_opinion(state)
452
- return (gr.update(visible=False), gr.update(visible=True), panel, to_print, override)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
 
455
  def ack_override(state):
@@ -475,6 +538,79 @@ def job_readout(state):
475
  f"{icon('printer')} {PRINTER}</div>")
476
 
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  def _simulated_result_panel(sess, run_summary, material, geometry_type, env, label) -> str:
479
  """Two-zone outcome β€” the dominant SIMULATED RESULT zone (the compact LOG A REAL
480
  PRINT zone is static UI below). Shows the final outcome, the climb, whether the
@@ -503,36 +639,63 @@ def _simulated_result_panel(sess, run_summary, material, geometry_type, env, lab
503
  )
504
 
505
 
506
- def run_print(state, iterations):
507
- """PRINT: run THIS job (inherited from Build) through the closed loop. Each
508
- iteration: policy proposes β†’ Spine vetoes β†’ the deterministic world prints β†’
509
- the Inspector grades that outcome β†’ policy + ledger learn. Slider 1 = a single
510
- print. Results reveal only after the run (progressive reveal)."""
511
  if not state or "job" not in state:
512
  gr.Warning("Build a job first (BUILD β†’ SLICE), then print it here.")
513
- return (gr.update(),) * 9
 
514
  job = Job(**state["job"])
515
  env = Environment(**state["env"])
516
  material, geometry_type = job.material, job.geometry_type
517
  key = cell_key(material, geometry_type, env)
518
  before_html = policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key)
519
 
520
- sess = run_session(job, env, int(iterations), POLICY, LEDGER)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  after = POLICY.cell_stats(material, geometry_type, env)
522
- verdicts = [inspector.grade_iteration(geometry_type, r.result) for r in sess.records]
523
- run_summary = inspector.summarize_run(sess.records, material=material, geometry=geometry_type)
524
 
525
- traj = sess.trajectory
526
- first = sess.first_success
527
  field_log.log_event("print_run", {"material": material, "geometry": geometry_type,
528
  "env_temp": env.temp, "env_humidity": env.humidity,
529
- "iterations": len(sess.records), "q_start": round(traj[0], 3),
530
  "q_end": round(traj[-1], 3), "first_clean": first,
531
- "inspector_stance": run_summary.stance})
 
 
532
  # ── deliberation log: World simulates -> La Forge grades, per iteration; then verdict ──
533
  ctx = _delib_ctx(job, env)
534
  loop_turns = []
535
- for r, g in zip(sess.records, verdicts):
536
  clamp = " (Spine clamped a setting)" if r.clamped else ""
537
  loop_turns.append({"agent": "World", "act": "simulate", "stance": r.result.outcome,
538
  "content": f"Iteration {r.n}: {r.result.detail}.{clamp} Policy: {r.learned}."})
@@ -545,7 +708,7 @@ def run_print(state, iterations):
545
  ], ctx)
546
  headline = (
547
  f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β€” "
548
- f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
549
  + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
550
  if first else f"still improving β€” best **{max(traj):.2f}** after {len(traj)} runs.")
551
  + " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
@@ -553,16 +716,18 @@ def run_print(state, iterations):
553
  )
554
  policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
555
  f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
556
- outcome = _simulated_result_panel(sess, run_summary, material, geometry_type, env, state.get("label"))
557
- return (
558
- gr.update(visible=True), # results_group (reveal)
559
- headline, # p_headline
560
- quality_curve_html(traj), # p_curve
561
- iteration_log_html(sess.records, verdicts), # p_log (with inspector grades)
562
- policy_html, # p_policy
563
- outcome, # outcome_panel (SIMULATED RESULT zone)
564
- ledger_html(), # ledger_panel
565
- render_node_cards(env, working=False), # node_cards
 
 
566
  inspector_panel(run_summary, label="LA FORGE Β· RUN VERDICT"), # review_summary
567
  )
568
 
@@ -591,35 +756,57 @@ def launch(**kw):
591
 
592
 
593
  def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="primary",
594
- primary_id=None):
595
  """Build the consistent top-right action bar (small primary + persistent Reset).
596
- Both buttons are scale=0 so they DON'T stretch full-width."""
 
597
  with gr.Row(elem_classes=["ce-actionbar"]):
598
  reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)
599
  primary = None
600
  if primary_label:
 
 
 
601
  primary = gr.Button(primary_label, variant=primary_variant,
602
- elem_classes=["ce-act"], elem_id=primary_id, scale=0)
603
  return reset, primary
604
 
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  # outputs touched by Reset (shared by the persistent Reset on every tab)
607
  def _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
608
- p_headline, outcome_panel, results_group, real_log_msg, studio_log):
 
 
609
  return [ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
610
- p_headline, outcome_panel, results_group, real_log_msg, studio_log]
 
611
 
612
 
613
  def build() -> gr.Blocks:
614
  with gr.Blocks(title="Microfactory Node: 3D Printer") as demo:
615
  gr.HTML(command_bar(llm.backend_status()))
616
- # header row: dropdown + warm button only (NO background tints, NO stretching)
617
  with gr.Row(elem_id="ce-modelswitch"):
618
  model_select = gr.Dropdown(MODEL_OPTIONS, value=MODEL_OPTIONS[0],
619
  show_label=False, container=False,
620
  elem_classes=["ce-modeldd"], scale=0)
621
  warm_btn = gr.Button("WARM UP", elem_classes=["ce-pillbtn", "ce-icon-bolt"], scale=0)
622
- model_status = gr.HTML(_status_html(), elem_classes=["ce-status-inline"])
623
 
624
  state = gr.State()
625
  part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
@@ -627,9 +814,22 @@ def build() -> gr.Blocks:
627
  with gr.Tabs() as tabs:
628
  # ───────────────────────── BUILD Β· define the job ────────────────────────
629
  with gr.Tab("LOAD", id="studio"):
630
- # Top row: ENVIRONMENT readout (left) + RESET/SLICE (right) on the same line.
 
631
  with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):
632
  sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])
 
 
 
 
 
 
 
 
 
 
 
 
633
  roll_btn = gr.Button("RANDOMIZE",
634
  elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)
635
  gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "
@@ -639,26 +839,10 @@ def build() -> gr.Blocks:
639
  "text-transform:uppercase;letter-spacing:.5px;font-size:12px;"
640
  "font-weight:700;cursor:pointer;display:inline-flex;"
641
  "align-items:center;gap:6px;'>OVERRIDE</button>")
642
- # spacer pushes the action buttons to the right edge
643
- gr.HTML("<div style='flex:1;'></div>")
644
  reset_s = gr.Button("RESET", elem_classes=["ce-pillbtn", "ce-act"], scale=0)
645
  run_btn = gr.Button("SLICE", variant="primary",
646
  elem_classes=["ce-act"], elem_id="ce-run", scale=0)
647
 
648
- # 2-column setup: BUILD-PLATE POSITION | MATERIAL
649
- # (NOTES moved into PART card; ENVIRONMENT moved into action bar above)
650
- with gr.Row(equal_height=False, elem_classes=["ce-loadrow"]):
651
- with gr.Column(scale=2, min_width=320):
652
- gr.HTML(rule("BUILD-PLATE POSITION"))
653
- gr.HTML("<div class='ce-sub' style='margin:-2px 0 4px;'>"
654
- "edges/corners run cooler β†’ warp/adhesion risk</div>")
655
- bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
656
- elem_classes=["ce-pills"])
657
- with gr.Column(scale=1, min_width=220):
658
- gr.HTML(rule("MATERIAL"))
659
- material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
660
- gr.HTML(_model_callout())
661
-
662
  # OVERRIDE ENVIRONMENT popup (hidden by default; toggled via JS)
663
  gr.HTML("<div class='ce-popup-backdrop' data-popup-backdrop='override'></div>")
664
  with gr.Group(elem_classes=["ce-popup"], elem_id="ce-popup-override"):
@@ -679,17 +863,13 @@ def build() -> gr.Blocks:
679
  with gr.Column(scale=3, elem_classes=["ce-part-viewer-col"]):
680
  part_placeholder = gr.HTML(_viewer_placeholder())
681
  model3d = gr.Model3D(value=None, label="", height=360, visible=False)
682
- # NOTES moved here β€” lives next to the part view always
683
- description = gr.Textbox(label="NOTES (OPTIONAL)",
684
- placeholder="e.g. 45Β° bracket, 60mm tall",
685
- elem_classes=["ce-notes-inline"])
686
  # intro + job log moved DOWN into the part area
687
  gr.HTML(tab_intro("Load the part, set the material and the room, then "
688
  "<b>SLICE</b> to read the engineer's pre-flight check. "
689
  "You give it the part, the material, and the room; it "
690
  "infers what kind of part this is."))
691
  studio_log = gr.HTML(studio_log_html())
692
- # RIGHT: dropzone TOP, then two matching-style mesh-source buttons
693
  with gr.Column(scale=1, elem_classes=["ce-part-actions-col"]):
694
  mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
695
  label="UPLOAD MESH", show_label=False,
@@ -704,49 +884,55 @@ def build() -> gr.Blocks:
704
  show_label=False, elem_classes=["ce-pills"])
705
  gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
706
  gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"], scale=0)
 
 
 
707
 
708
  # ───────────────── SLICE Β· slice + analyze + pre-flight check ─────────────
709
  with gr.Tab("SLICE", id="build"):
710
  reset_b, to_print_btn = _action_bar(primary_label="PRINT (RUN ITERATIONS)")
711
  gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "
712
  "precedent, flag failures, and get a second opinion. Then β†’ PRINT."))
713
- build_loader = gr.HTML()
714
  with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:
715
  # slice + motion preview side by side β€” grouped like LOAD's PART card
716
  gr.HTML(rule("SLICE Β· CROSS-SECTION"))
717
  with gr.Row(equal_height=False, elem_classes=["ce-part-row"]):
718
  with gr.Column(scale=3):
719
  vp_layer = gr.Image(label="", height=360, show_label=False)
720
- with gr.Column(scale=1, elem_classes=["ce-vslider"]):
721
- vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1, label="LAYER")
722
- with gr.Column(scale=1, min_width=280):
723
  gr.HTML(rule("MOTION PREVIEW"))
724
  vprint = gr.HTML()
725
- gr.HTML("<div class='ce-sub' style='padding:0 12px;'>Slide the LAYER control through real cross-sections of "
726
- "<i>this</i> part at full mesh fidelity; the preview animates the rise.</div>")
 
 
 
 
727
 
728
  # Engineer's Read ↔ Second Opinion (one panel at a time)
729
- backend = gr.Markdown()
730
- gr.HTML(rule("THE READ"))
731
- read_toggle = gr.Radio(["Engineer's Read", "Second Opinion"],
732
- value="Engineer's Read", show_label=False,
733
- elem_classes=["ce-seg"])
734
- with gr.Group(visible=True, elem_classes=["ce-card"]) as eng_read_group:
735
- precedent = gr.HTML(elem_id="ce-precedent")
736
- reasoning = gr.Markdown(elem_id="ce-reasoning")
737
- risks = gr.HTML()
738
- gr.HTML(rule("VALIDATION + G-CODE"))
739
- spine_notes = gr.Markdown()
740
- with gr.Row(equal_height=False):
741
- settings_html = gr.HTML()
742
- gcode_html = gr.HTML()
743
- confirm_btn = gr.Button("CONFIRM & PRINT", elem_id="ce-confirm", visible=False)
744
- with gr.Group(visible=False, elem_classes=["ce-card"]) as second_op_group:
745
- gr.HTML("<div class='ce-sub' style='padding:0 12px;'>A separate inspector β€” <b>La Forge</b> β€” reviews "
 
 
746
  "the plan before it prints: O'Brien is an optimist, La Forge is not.</div>")
747
- second_opinion_panel = gr.HTML()
748
- override_btn = gr.Button("PRINT ANYWAY (I'VE REVIEWED THE OBJECTION)",
749
- visible=False, elem_classes=["ce-pillbtn"])
750
 
751
  # ──────────────────── PRINT Β· run it, iterate, grade ─────────────────────
752
  with gr.Tab("PRINT", id="print"):
@@ -754,27 +940,55 @@ def build() -> gr.Blocks:
754
  gr.HTML(tab_intro("Print <b>this job</b> (inherited from Build). The Engineer proposes β†’ "
755
  "the Spine vetoes β†’ a <b>simulated world</b> prints β†’ the <b>Inspector "
756
  "grades</b> β†’ policy + ledger learn. Quality compounds failβ†’clean."))
757
- p_job = gr.HTML(job_readout(None))
758
- with gr.Group(elem_classes=["ce-card"]):
759
- gr.HTML(rule("RUN"))
760
- p_iters = gr.Slider(1, 16, value=8, step=1,
761
- label="ITERATIONS (1 = a single print)")
762
- gr.HTML("<div class='ce-sub'>Press <b>PRINT</b> (top right) to run. The Inspector "
763
- "grades each run as part of the iteration.</div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
 
765
  with gr.Group(visible=False) as results_group:
 
 
766
  p_headline = gr.Markdown()
767
- with gr.Row():
768
- with gr.Column(scale=3):
769
- gr.HTML(rule("QUALITY PER ITERATION"))
770
- p_curve = gr.HTML()
771
- with gr.Column(scale=2):
772
- gr.HTML(rule("ITERATION LOG"))
773
- p_log = gr.HTML()
774
  gr.HTML(rule("LEARNED POLICY CELL"))
775
  p_policy = gr.HTML()
776
- gr.HTML(rule("OUTCOME"))
777
- outcome_panel = gr.HTML()
778
  # compact secondary zone (~20%): log a REAL print back into the ledger
779
  with gr.Row(elem_classes=["ce-card"]):
780
  with gr.Column(scale=1):
@@ -810,7 +1024,8 @@ def build() -> gr.Blocks:
810
  # ── wiring ──
811
  reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
812
  p_log, p_headline, outcome_panel, results_group, real_log_msg,
813
- studio_log)
 
814
  preview_outs = [part, model3d, part_status, part_placeholder]
815
  foot_in = [part, material, temp, humidity, bed_position]
816
  benchy_btn.click(load_benchy, None, preview_outs).then(status_footer, foot_in, [footer])
@@ -836,11 +1051,12 @@ def build() -> gr.Blocks:
836
 
837
  # Two-step BUILD: instant loader + tab-switch, then the heavy model call (reveals
838
  # the results), then refresh the inherited-job readout on the Print tab.
839
- build_start_outs = [tabs, build_loader, build_results, to_print_btn, override_btn,
840
- second_opinion_panel, read_toggle, eng_read_group, second_op_group]
 
841
  build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
842
  gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,
843
- build_loader, build_results]
844
  run_btn.click(build_start, [part], build_start_outs).then(
845
  build_job, [part, material, description, temp, humidity, bed_position, model_select],
846
  build_outs).then(job_readout, [state], [p_job])
@@ -848,17 +1064,22 @@ def build() -> gr.Blocks:
848
  vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
849
  read_toggle.change(toggle_read, [read_toggle, state],
850
  [eng_read_group, second_op_group, second_opinion_panel,
851
- to_print_btn, override_btn])
852
  override_btn.click(ack_override, state, [to_print_btn, override_btn])
853
  to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
854
  None, None, None, js=_SCROLL_TOP)
855
- tabs.select(job_readout, [state], [p_job])
856
 
857
  # PRINT: run the loop on THIS job (slider 1 = single print), or log a real outcome.
858
- print_outs = [results_group, p_headline, p_curve, p_log, p_policy, outcome_panel,
859
  ledger_panel, node_cards, review_summary]
860
- p_run.click(run_print, [state, p_iters], print_outs).then(
 
861
  None, None, None, js=_SCROLL_TOP)
 
 
 
 
862
  for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
863
  btn.click(record_outcome, [gr.State(oc), state], [real_log_msg, ledger_panel, node_cards])
864
 
 
18
 
19
  import os
20
  import random
21
+ import time
22
  import uuid
23
+ from pathlib import Path
24
 
25
  import gradio as gr
26
 
 
50
  from core.nodes import render_node_cards
51
  from core.reflect import reflect_on_job
52
  from core.spine import SpineValidator
53
+ from learn.loop import run_iteration, SessionResult
54
  from learn.policy import LearnedPolicy, cell_key
55
  from core.viewer import (
56
  GEO_READS,
 
176
 
177
  def reset_learnings():
178
  """Reset the live ledger + learned policy to the curated baseline (seed + ingested),
179
+ clearing this session's accumulated runs. Also clears the loaded part and hides the
180
+ result groups so the next run starts from a clean LOAD tab."""
181
  removed = LEDGER.reset_to_baseline()
182
  POLICY.reset()
183
  gr.Info(f"Reset to baseline β€” cleared {removed} runtime lesson(s) + the learned policy.")
 
191
  gr.update(visible=False), # print results_group (re-hide)
192
  "", # real_log_msg
193
  studio_log_html(), # studio_log
194
+ {"geometry": None, "mesh": None, "label": None, "read": None}, # part state
195
+ gr.update(value=None, visible=False), # model3d
196
+ gr.update(visible=True), # part_placeholder
197
+ "", # part_status
198
+ gr.update(visible=False), # build_results
199
+ gr.update(visible=False), # read_results
200
+ gr.Tabs(selected="studio"), # tabs back to LOAD
201
  )
202
 
203
 
 
229
 
230
 
231
  def build_start(part):
232
+ """Instant feedback when BUILD is clicked: jump to Build, reveal the slicer +
233
+ virtual print preview immediately, and show the read panel in a loading state
234
+ while the model runs. No part β†’ stay put."""
235
  if not (part and part.get("geometry")):
236
  gr.Warning("Load a part on the BUILD tab first β€” quick-load Benchy, generate a primitive, or drop a mesh.")
237
+ return (gr.update(),) * 13
238
+ mesh = part.get("mesh")
239
+ label = part.get("label") or "PART"
240
+ vp_html = virtual_printer_html(mesh, caption=f"{label} Β· virtual print preview")
241
+ layer_img = layer_image(mesh, 1)
242
  return (
243
  gr.Tabs(selected="build"), # tabs
244
+ gr.update(visible=True), # build_results (reveal)
245
+ gr.update(visible=True), # read_results (reveal, loading state)
246
+ loader("READING THE PLAN Β· O'Brien is checking precedent and proposing settings"),
247
+ # read_loader
248
+ vp_html, # vprint
249
+ gr.update(value=1), # vp_slider reset
250
+ layer_img, # vp_layer
251
  gr.update(interactive=True), # to_print_btn (un-gate)
252
  gr.update(visible=False), # override_btn (hide)
253
+ "", # second_opinion_panel (clear stale)
254
  gr.update(value="Engineer's Read"), # read_toggle (reset to the read)
255
+ gr.update(visible=True), # eng_read_group
256
+ gr.update(visible=False), # second_op_group
257
  )
258
 
259
 
 
291
 
292
 
293
  def warm_up_pending() -> str:
294
+ """Header icon stays; the warm-up intent is signaled by a toast."""
295
+ gr.Info("Warming up the model (first load can take ~30s on ZeroGPU)…")
296
+ return _model_icon()
297
 
298
 
299
  def warm_up_cb() -> str:
300
+ """After warm-up, keep the info icon in the header."""
301
+ return _model_icon()
302
+
303
+
304
+ def _model_icon() -> str:
305
+ """Small info-icon callout for the header row. No text β€” the dropdown + warm-up
306
+ button already give enough context; the tooltip explains the backends."""
307
+ return (
308
+ "<span class='ce-callout' tabindex='0'>" + icon("info") + ""
309
+ "<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "
310
+ "<b>Base</b> is the stock Gemma 4 E4B; <b>Modal API</b> is the remote endpoint. "
311
+ "Local users: pull the LoRA from HF Hub "
312
+ "(kylebrodeur/microfactory-node-lora-v2 / -v3-qat) or via ollama.</span></span>"
313
+ )
314
 
315
 
316
  def _model_callout() -> str:
317
+ """Legacy full-width model callout (used when more explanatory text is wanted)."""
318
  return (
319
  "<span class='ce-callout' tabindex='0'>" + icon("info") + " MODEL INFO"
320
  "<span class='ce-tip'>The <b>LoRA v2/v3</b> adapters serve via ZeroGPU on the Space; "
 
325
 
326
 
327
  def select_model(choice: str) -> str:
328
+ """Model switcher: apply the chosen backend, then return the info icon so the
329
+ header stays compact."""
 
 
330
  choice = choice or MODEL_OPTIONS[0]
331
  try:
332
  _apply_model_choice(choice)
333
  except Exception:
334
  pass
335
+ return _model_icon()
336
 
337
 
338
  # ── simulated environment (this is a sim lab β€” conditions are generated, overridable) ──
 
389
  rec = advise(job, env, retrieved, references, policy_note)
390
  spine = SPINE.check(rec.advice.settings, material)
391
  hint = steepest_overhang_hint(mesh) if geometry_type in ("overhang", "bridge") else None
392
+ vp_html = virtual_printer_html(mesh, settings=spine.settings,
393
+ caption=f"{material} Β· {geometry_type}") # init on BUILD
394
 
395
  precedent = precedent_eval_html(retrieved, env)
396
  if retrieved:
 
407
  confirm_vis = gr.update(visible=spine.requires_approval)
408
  approval_md = ("**HITL gate:** the Spine clamped a boundary setting β€” review, then **Confirm & Print**."
409
  if spine.requires_approval else "Within safe envelope β€” ready when you are.")
410
+ spine_notes_text = f"{spine_md}\n\n{approval_md}" if spine_md else approval_md
411
  session_id = uuid.uuid4().hex
412
  state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
413
+ "advice": rec.advice.model_dump(), "label": part.get("label"), "session_id": session_id,
414
+ "spine_notes": spine_notes_text}
415
 
416
  # ── field log (Space only β€” gated on HF_TOKEN; local/offline no-ops) ──
417
  field_log.log_build(
 
436
  f"**Chief Engineer O'Brien:** {rec.advice.reasoning}", # reasoning
437
  risk_callouts_html(rec.advice.risks, hint) + placement_callout(material, bed_position), # risks
438
  settings_panel_html(spine.settings, material), # settings (LCARS panel)
439
+ spine_notes_text, # spine notes
440
  gcode_panel_html(spine.settings, material), # g-code (LCARS panel)
441
  confirm_vis, # confirm visibility
442
  render_node_cards(env, working=True), # node cards
443
  state, # state
444
+ virtual_printer_html(mesh, settings=spine.settings,
445
+ caption=f"{material} Β· {geometry_type}"), # vprint (refined caption)
446
  gr.update(value=1), # reset layer scrubber
447
  layer_image(mesh, 1), # initial scrubbed layer
448
+ "", # read_loader (clear)
 
449
  )
450
 
451
 
452
+ def _compute_second_opinion(state):
453
+ """Run the Inspector (La Forge) critique once. Returns the raw tuple used by
454
+ the UI: (panel_html, to_print_update, override_update)."""
455
  if not state or "advice" not in state:
456
  return ("<div class='ce-sub'>Build a job first β€” then I'll give the plan a second look.</div>",
457
  gr.update(interactive=True), gr.update(visible=False))
 
477
  return panel, gr.update(interactive=True), gr.update(visible=False)
478
 
479
 
480
+ def second_opinion(state):
481
+ """Idempotent wrapper: if La Forge already weighed in on this build, return the
482
+ cached verdict; otherwise compute it once and store it in state."""
483
+ if state and state.get("second_opinion"):
484
+ return state["second_opinion"]
485
+ result = _compute_second_opinion(state)
486
+ if state:
487
+ state["second_opinion"] = result
488
+ return result
489
+
490
+
491
  def toggle_read(choice, state):
492
  """Segmented toggle on Build: flip between Engineer's Read and Second Opinion,
493
+ showing one panel at a time. The opinion is computed lazily on first reveal and
494
+ then cached for the rest of the build. While it computes, the Second Opinion panel
495
+ shows a mini-loader so the user knows La Forge is reviewing the plan."""
496
  if str(choice).lower().startswith("engineer"):
497
+ yield (gr.update(visible=True), gr.update(visible=False),
498
+ "", gr.update(), gr.update(), state)
499
+ return
500
+ cached = state.get("second_opinion") if state else None
501
+ if cached:
502
+ panel, to_print, override = cached
503
+ yield (gr.update(visible=False), gr.update(visible=True), panel, to_print, override, state)
504
+ return
505
+ # show in-place loader while the Inspector runs
506
+ yield (gr.update(visible=False), gr.update(visible=True),
507
+ loader("CONSULTING LA FORGE", stages=[
508
+ "reading the engineer's plan",
509
+ "checking precedent and risks",
510
+ "comparing settings to the safe envelope",
511
+ "drafting the pre-print verdict",
512
+ ]), gr.update(interactive=True), gr.update(visible=False), state)
513
+ panel, to_print, override = _compute_second_opinion(state)
514
+ state = (state or {}) | {"second_opinion": (panel, to_print, override)}
515
+ yield (gr.update(visible=False), gr.update(visible=True), panel, to_print, override, state)
516
 
517
 
518
  def ack_override(state):
 
538
  f"{icon('printer')} {PRINTER}</div>")
539
 
540
 
541
+ def plan_card_html(state):
542
+ """THE PLAN card: Spine-validated proposed settings + Spine notes for the current job."""
543
+ if not state or "settings" not in state:
544
+ return ("<div class='ce-sub'>No plan built yet. Go to <b>BUILD</b> β†’ define a part β†’ "
545
+ "<b>SLICE</b>, then return here to print it.</div>")
546
+ settings = PrintSettings(**state["settings"])
547
+ material = state["job"]["material"]
548
+ html = settings_panel_html(settings, material)
549
+ notes = state.get("spine_notes") or ""
550
+ if notes:
551
+ html += ("<div class='ce-sub' style='margin-top:6px;'>" +
552
+ notes.replace("\n", "<br>") + "</div>")
553
+ return html
554
+
555
+
556
+ def plan_verdict_html(state):
557
+ """La Forge's pre-print stance if already computed on the SLICE tab."""
558
+ if not state or "settings" not in state:
559
+ return ("<div class='ce-sub'>Build a job first to get La Forge's pre-print stance.</div>")
560
+ cached = state.get("second_opinion")
561
+ if cached:
562
+ return cached[0]
563
+ return ("<div class='ce-sub'>" + icon("search") +
564
+ " Run the Inspector (<b>Second Opinion</b>) on the SLICE tab for a pre-print stance.</div>")
565
+
566
+
567
+ def _print_plan_values(state):
568
+ """Return the Engineer's proposed settings values (or safe defaults) for VARY sliders."""
569
+ defaults = {"nozzle_temp": 200, "bed_temp": 60, "fan_pct": 80, "retraction_mm": 4.5}
570
+ if not state or "settings" not in state:
571
+ return defaults
572
+ s = state["settings"]
573
+ return {k: s.get(k, defaults[k]) for k in defaults}
574
+
575
+
576
+ def apply_overrides(state, nozzle, bed, fan, retract):
577
+ """Compare VARY slider values to the Engineer's plan. If anything changed, build an
578
+ override PrintSettings and log that the operator defied the plan. Returns the override
579
+ dict (or None when the plan is unchanged)."""
580
+ if not state or "settings" not in state:
581
+ return None
582
+ plan = PrintSettings(**state["settings"])
583
+ diffs = {}
584
+ if abs(float(nozzle) - plan.nozzle_temp) > 1e-6:
585
+ diffs["nozzle_temp"] = float(nozzle)
586
+ if abs(float(bed) - plan.bed_temp) > 1e-6:
587
+ diffs["bed_temp"] = float(bed)
588
+ if abs(float(fan) - plan.fan_pct) > 1e-6:
589
+ diffs["fan_pct"] = float(fan)
590
+ if abs(float(retract) - plan.retraction_mm) > 1e-6:
591
+ diffs["retraction_mm"] = float(retract)
592
+ if not diffs:
593
+ state.pop("print_overrides", None)
594
+ return None
595
+ overrides = plan.model_copy(update=diffs)
596
+ state["print_overrides"] = overrides.model_dump()
597
+ job = Job(**state["job"])
598
+ env = Environment(**state["env"])
599
+ field_log.log_event("print_override", {
600
+ "material": job.material, "geometry": job.geometry_type,
601
+ "env_temp": env.temp, "env_humidity": env.humidity,
602
+ "settings": overrides.model_dump(),
603
+ })
604
+ if state.get("session_id"):
605
+ deliberation_log.log_turns(state["session_id"], "preflight", [
606
+ {"agent": "Operator", "act": "override",
607
+ "content": (f"Overrode the Engineer: nozzle {overrides.nozzle_temp:.0f}Β°C, "
608
+ f"bed {overrides.bed_temp:.0f}Β°C, fan {overrides.fan_pct:.0f}%, "
609
+ f"retraction {overrides.retraction_mm:.1f}mm.")},
610
+ ], _delib_ctx(job, env))
611
+ return overrides.model_dump()
612
+
613
+
614
  def _simulated_result_panel(sess, run_summary, material, geometry_type, env, label) -> str:
615
  """Two-zone outcome β€” the dominant SIMULATED RESULT zone (the compact LOG A REAL
616
  PRINT zone is static UI below). Shows the final outcome, the climb, whether the
 
639
  )
640
 
641
 
642
+ def run_print(state, iterations, nozzle, bed, fan, retract, progress=gr.Progress()):
643
+ """PRINT: run THIS job (inherited from Build) through the closed loop.
644
+ Now a generator so the iteration log + quality chart + policy cell fill in
645
+ live as each iteration completes. Per-iteration timing is shown next to each row."""
 
646
  if not state or "job" not in state:
647
  gr.Warning("Build a job first (BUILD β†’ SLICE), then print it here.")
648
+ yield (gr.update(),) * 9
649
+ return
650
  job = Job(**state["job"])
651
  env = Environment(**state["env"])
652
  material, geometry_type = job.material, job.geometry_type
653
  key = cell_key(material, geometry_type, env)
654
  before_html = policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key)
655
 
656
+ overrides_dict = apply_overrides(state, nozzle, bed, fan, retract)
657
+ overrides = PrintSettings(**overrides_dict) if overrides_dict else None
658
+
659
+ records, verdicts, timings = [], [], []
660
+ n_iters = int(iterations)
661
+ start_iter = time.perf_counter()
662
+ for i in range(1, n_iters + 1):
663
+ progress(i / n_iters, desc=f"Iteration {i}/{n_iters}")
664
+ record = run_iteration(job, env, POLICY, LEDGER, i, overrides=overrides)
665
+ verdict = inspector.grade_iteration(geometry_type, record.result)
666
+ elapsed = time.perf_counter() - start_iter
667
+ start_iter = time.perf_counter()
668
+ records.append(record)
669
+ verdicts.append(verdict)
670
+ timings.append(elapsed)
671
+ yield (
672
+ gr.update(visible=True), # results_group
673
+ "", # outcome_panel
674
+ "", # p_headline
675
+ quality_curve_html([r.result.quality for r in records]), # p_curve
676
+ iteration_log_html(records, verdicts, timings), # p_log
677
+ policy_cell_html(POLICY.cell_stats(material, geometry_type, env), key), # p_policy
678
+ gr.update(), # ledger_panel
679
+ gr.update(), # node_cards
680
+ "", # review_summary
681
+ )
682
+
683
  after = POLICY.cell_stats(material, geometry_type, env)
684
+ run_summary = inspector.summarize_run(records, material=material, geometry=geometry_type)
 
685
 
686
+ traj = [r.result.quality for r in records]
687
+ first = next((r.n for r in records if r.result.outcome == "success"), None)
688
  field_log.log_event("print_run", {"material": material, "geometry": geometry_type,
689
  "env_temp": env.temp, "env_humidity": env.humidity,
690
+ "iterations": len(records), "q_start": round(traj[0], 3),
691
  "q_end": round(traj[-1], 3), "first_clean": first,
692
+ "inspector_stance": run_summary.stance,
693
+ "used_override": overrides is not None,
694
+ "override_settings": overrides.model_dump() if overrides else None})
695
  # ── deliberation log: World simulates -> La Forge grades, per iteration; then verdict ──
696
  ctx = _delib_ctx(job, env)
697
  loop_turns = []
698
+ for r, g in zip(records, verdicts):
699
  clamp = " (Spine clamped a setting)" if r.clamped else ""
700
  loop_turns.append({"agent": "World", "act": "simulate", "stance": r.result.outcome,
701
  "content": f"Iteration {r.n}: {r.result.detail}.{clamp} Policy: {r.learned}."})
 
708
  ], ctx)
709
  headline = (
710
  f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β€” "
711
+ f"started at quality **{traj[0]:.2f}** ({records[0].result.outcome}); "
712
  + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
713
  if first else f"still improving β€” best **{max(traj):.2f}** after {len(traj)} runs.")
714
  + " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
 
716
  )
717
  policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
718
  f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
719
+ outcome = _simulated_result_panel(
720
+ SessionResult(job=job, env=env, records=records),
721
+ run_summary, material, geometry_type, env, state.get("label"))
722
+ yield (
723
+ gr.update(visible=True), # results_group
724
+ outcome, # outcome_panel
725
+ headline, # p_headline
726
+ quality_curve_html(traj), # p_curve
727
+ iteration_log_html(records, verdicts, timings), # p_log
728
+ policy_html, # p_policy
729
+ ledger_html(), # ledger_panel
730
+ render_node_cards(env, working=False), # node_cards
731
  inspector_panel(run_summary, label="LA FORGE Β· RUN VERDICT"), # review_summary
732
  )
733
 
 
756
 
757
 
758
  def _action_bar(reset_btn_label="RESET", primary_label=None, primary_variant="primary",
759
+ primary_id=None, primary_arrow=True):
760
  """Build the consistent top-right action bar (small primary + persistent Reset).
761
+ Both buttons are scale=0 so they DON'T stretch full-width. Primary buttons get
762
+ a right-side arrow icon by default to signal "proceed to next stage"."""
763
  with gr.Row(elem_classes=["ce-actionbar"]):
764
  reset = gr.Button(reset_btn_label, elem_classes=["ce-pillbtn", "ce-act"], scale=0)
765
  primary = None
766
  if primary_label:
767
+ primary_classes = ["ce-act"]
768
+ if primary_arrow:
769
+ primary_classes.append("ce-icon-arrow-after")
770
  primary = gr.Button(primary_label, variant=primary_variant,
771
+ elem_classes=primary_classes, elem_id=primary_id, scale=0)
772
  return reset, primary
773
 
774
 
775
+ def refresh_print_tab(state):
776
+ """Refresh the PRINT tab's plan + verdict readouts whenever the tab is visited."""
777
+ return job_readout(state), plan_card_html(state), plan_verdict_html(state)
778
+
779
+
780
+ def open_vary_panel(state):
781
+ """Open the VARY override panel and seed the sliders with the Engineer's plan."""
782
+ vals = _print_plan_values(state)
783
+ return (gr.update(visible=True),
784
+ gr.update(value=vals["nozzle_temp"]),
785
+ gr.update(value=vals["bed_temp"]),
786
+ gr.update(value=vals["fan_pct"]),
787
+ gr.update(value=vals["retraction_mm"]))
788
+
789
+
790
  # outputs touched by Reset (shared by the persistent Reset on every tab)
791
  def _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
792
+ p_headline, outcome_panel, results_group, real_log_msg, studio_log,
793
+ part, model3d, part_placeholder, part_status, build_results,
794
+ read_results, tabs):
795
  return [ledger_panel, node_cards, review_summary, p_curve, p_policy, p_log,
796
+ p_headline, outcome_panel, results_group, real_log_msg, studio_log,
797
+ part, model3d, part_placeholder, part_status, build_results, read_results, tabs]
798
 
799
 
800
  def build() -> gr.Blocks:
801
  with gr.Blocks(title="Microfactory Node: 3D Printer") as demo:
802
  gr.HTML(command_bar(llm.backend_status()))
803
+ # header row: dropdown + warm button + info icon only (NO background tints, NO stretching)
804
  with gr.Row(elem_id="ce-modelswitch"):
805
  model_select = gr.Dropdown(MODEL_OPTIONS, value=MODEL_OPTIONS[0],
806
  show_label=False, container=False,
807
  elem_classes=["ce-modeldd"], scale=0)
808
  warm_btn = gr.Button("WARM UP", elem_classes=["ce-pillbtn", "ce-icon-bolt"], scale=0)
809
+ model_status = gr.HTML(_model_icon(), elem_classes=["ce-status-inline"])
810
 
811
  state = gr.State()
812
  part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
 
814
  with gr.Tabs() as tabs:
815
  # ───────────────────────── BUILD Β· define the job ────────────────────────
816
  with gr.Tab("LOAD", id="studio"):
817
+ # Top row: ENVIRONMENT + POSITION + MATERIAL readouts on the left,
818
+ # RANDOMIZE / OVERRIDE / RESET / SLICE on the right.
819
  with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):
820
  sensors_readout = gr.HTML(elem_classes=["ce-envbar-readout"])
821
+ with gr.Column(elem_classes=["ce-inline-group"]):
822
+ gr.HTML("<div class='ce-inline-label'>POSITION"
823
+ "<span class='ce-callout' tabindex='0'>" + icon("info", size=10) +
824
+ "<span class='ce-tip'>edges/corners run cooler β†’ warp/adhesion risk</span>"
825
+ "</span></div>")
826
+ bed_position = gr.Radio(BED_POSITIONS, value="center", show_label=False,
827
+ elem_classes=["ce-pills"])
828
+ with gr.Column(elem_classes=["ce-inline-group"]):
829
+ gr.HTML("<div class='ce-inline-label'>MATERIAL</div>")
830
+ material = gr.Radio(MATERIALS, value="PLA", show_label=False, elem_classes=["ce-pills"])
831
+ # spacer pushes the action buttons to the right edge
832
+ gr.HTML("<div style='flex:1;'></div>")
833
  roll_btn = gr.Button("RANDOMIZE",
834
  elem_classes=["ce-pillbtn", "ce-icon-shuffle"], scale=0)
835
  gr.HTML("<button class='ce-pillbtn ce-icon-sliders' "
 
839
  "text-transform:uppercase;letter-spacing:.5px;font-size:12px;"
840
  "font-weight:700;cursor:pointer;display:inline-flex;"
841
  "align-items:center;gap:6px;'>OVERRIDE</button>")
 
 
842
  reset_s = gr.Button("RESET", elem_classes=["ce-pillbtn", "ce-act"], scale=0)
843
  run_btn = gr.Button("SLICE", variant="primary",
844
  elem_classes=["ce-act"], elem_id="ce-run", scale=0)
845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  # OVERRIDE ENVIRONMENT popup (hidden by default; toggled via JS)
847
  gr.HTML("<div class='ce-popup-backdrop' data-popup-backdrop='override'></div>")
848
  with gr.Group(elem_classes=["ce-popup"], elem_id="ce-popup-override"):
 
863
  with gr.Column(scale=3, elem_classes=["ce-part-viewer-col"]):
864
  part_placeholder = gr.HTML(_viewer_placeholder())
865
  model3d = gr.Model3D(value=None, label="", height=360, visible=False)
 
 
 
 
866
  # intro + job log moved DOWN into the part area
867
  gr.HTML(tab_intro("Load the part, set the material and the room, then "
868
  "<b>SLICE</b> to read the engineer's pre-flight check. "
869
  "You give it the part, the material, and the room; it "
870
  "infers what kind of part this is."))
871
  studio_log = gr.HTML(studio_log_html())
872
+ # RIGHT: dropzone + mesh-source buttons + NOTES
873
  with gr.Column(scale=1, elem_classes=["ce-part-actions-col"]):
874
  mesh_in = gr.File(file_types=[".stl", ".glb", ".obj"],
875
  label="UPLOAD MESH", show_label=False,
 
884
  show_label=False, elem_classes=["ce-pills"])
885
  gen_size = gr.Number(value=30, label="SIZE (mm)", elem_classes=["ce-num"])
886
  gen_btn = gr.Button("GENERATE", elem_classes=["ce-pillbtn"], scale=0)
887
+ description = gr.Textbox(label="NOTES (OPTIONAL)",
888
+ placeholder="e.g. 45Β° bracket, 60mm tall",
889
+ elem_classes=["ce-notes-inline"])
890
 
891
  # ───────────────── SLICE Β· slice + analyze + pre-flight check ─────────────
892
  with gr.Tab("SLICE", id="build"):
893
  reset_b, to_print_btn = _action_bar(primary_label="PRINT (RUN ITERATIONS)")
894
  gr.HTML(tab_intro("The pre-flight check, <b>before it prints</b>: slice the part, read "
895
  "precedent, flag failures, and get a second opinion. Then β†’ PRINT."))
 
896
  with gr.Group(visible=False, elem_classes=["ce-part-card"]) as build_results:
897
  # slice + motion preview side by side β€” grouped like LOAD's PART card
898
  gr.HTML(rule("SLICE Β· CROSS-SECTION"))
899
  with gr.Row(equal_height=False, elem_classes=["ce-part-row"]):
900
  with gr.Column(scale=3):
901
  vp_layer = gr.Image(label="", height=360, show_label=False)
902
+ with gr.Column(scale=1, elem_classes=["ce-vp"]):
 
 
903
  gr.HTML(rule("MOTION PREVIEW"))
904
  vprint = gr.HTML()
905
+ with gr.Column(elem_classes=["ce-hslider"]):
906
+ vp_slider = gr.Slider(1, SCRUB_LAYERS, value=1, step=1,
907
+ show_label=False, container=False)
908
+ gr.HTML("<div class='ce-sub' style='padding:0 12px;'>Scrub through real cross-sections of "
909
+ "<i>this</i> part at full mesh fidelity. The preview animates the layer rise "
910
+ "while the read loads below.</div>")
911
 
912
  # Engineer's Read ↔ Second Opinion (one panel at a time)
913
+ with gr.Group(visible=False, elem_classes=["ce-card"]) as read_results:
914
+ backend = gr.Markdown()
915
+ gr.HTML(rule("THE READ"))
916
+ read_loader = gr.HTML()
917
+ read_toggle = gr.Radio(["Engineer's Read", "Second Opinion"],
918
+ value="Engineer's Read", show_label=False,
919
+ elem_classes=["ce-seg"])
920
+ with gr.Group(visible=True, elem_classes=["ce-card"]) as eng_read_group:
921
+ precedent = gr.HTML(elem_id="ce-precedent")
922
+ reasoning = gr.Markdown(elem_id="ce-reasoning")
923
+ risks = gr.HTML()
924
+ gr.HTML(rule("VALIDATION + G-CODE"))
925
+ spine_notes = gr.Markdown()
926
+ with gr.Row(equal_height=False):
927
+ settings_html = gr.HTML()
928
+ gcode_html = gr.HTML()
929
+ confirm_btn = gr.Button("CONFIRM & PRINT", elem_id="ce-confirm", visible=False)
930
+ with gr.Group(visible=False, elem_classes=["ce-card"]) as second_op_group:
931
+ gr.HTML("<div class='ce-sub' style='padding:0 12px;'>A separate inspector β€” <b>La Forge</b> β€” reviews "
932
  "the plan before it prints: O'Brien is an optimist, La Forge is not.</div>")
933
+ second_opinion_panel = gr.HTML()
934
+ override_btn = gr.Button("PRINT ANYWAY (I'VE REVIEWED THE OBJECTION)",
935
+ visible=False, elem_classes=["ce-pillbtn"])
936
 
937
  # ──────────────────── PRINT Β· run it, iterate, grade ─────────────────────
938
  with gr.Tab("PRINT", id="print"):
 
940
  gr.HTML(tab_intro("Print <b>this job</b> (inherited from Build). The Engineer proposes β†’ "
941
  "the Spine vetoes β†’ a <b>simulated world</b> prints β†’ the <b>Inspector "
942
  "grades</b> β†’ policy + ledger learn. Quality compounds failβ†’clean."))
943
+
944
+ # ENVBAR: job readout | iteration chip | VARY
945
+ with gr.Row(elem_classes=["ce-actionbar", "ce-envbar"]):
946
+ p_job = gr.HTML(job_readout(None), elem_classes=["ce-envbar-readout"])
947
+ p_iter_readout = gr.HTML(
948
+ "<div class='ce-sub' style='text-align:center;'><b>8</b> ITERATIONS</div>")
949
+ gr.HTML("<div style='flex:1;'></div>")
950
+ vary_btn = gr.Button("VARY", elem_classes=["ce-pillbtn"], scale=0)
951
+
952
+ # THE PLAN card
953
+ with gr.Group(elem_classes=["ce-card"]) as plan_card:
954
+ gr.HTML(rule("THE PLAN"))
955
+ plan_settings = gr.HTML()
956
+ plan_verdict = gr.HTML()
957
+
958
+ # ITERATIONS hslider + explainer
959
+ with gr.Column(elem_classes=["ce-hslider"]):
960
+ p_iters = gr.Slider(1, 16, value=8, step=1, show_label=False, container=False)
961
+ gr.HTML("<div class='ce-sub' style='margin:-8px 0 10px;'>"
962
+ "1 = single print Β· 4 = quick convergence Β· 8 = balanced climb Β· 16 = full convergence run"
963
+ "</div>")
964
+
965
+ # VARY override panel
966
+ with gr.Group(visible=False, elem_classes=["ce-card"]) as vary_group:
967
+ gr.HTML(rule("OPERATOR OVERRIDE"))
968
+ gr.HTML("<div class='ce-sub' style='border-left:3px solid var(--ao-red);padding:6px 10px;'>"
969
+ + icon("alert") + " You are overriding the Engineer's plan. The run will use "
970
+ "these settings instead of the Spine-validated proposal.</div>")
971
+ with gr.Row():
972
+ p_nozzle = gr.Slider(150, 300, value=200, step=1,
973
+ label="NOZZLE Β°C", elem_classes=["ce-num"])
974
+ p_bed = gr.Slider(40, 120, value=60, step=1,
975
+ label="BED Β°C", elem_classes=["ce-num"])
976
+ p_fan = gr.Slider(0, 100, value=80, step=1,
977
+ label="FAN %", elem_classes=["ce-num"])
978
+ p_retract = gr.Slider(0, 10, value=4.5, step=0.1,
979
+ label="RETRACTION mm", elem_classes=["ce-num"])
980
+ reset_plan_btn = gr.Button("RESET TO ENGINEER'S PLAN", elem_classes=["ce-pillbtn"])
981
 
982
  with gr.Group(visible=False) as results_group:
983
+ gr.HTML(rule("OUTCOME"))
984
+ outcome_panel = gr.HTML()
985
  p_headline = gr.Markdown()
986
+ gr.HTML(rule("QUALITY PER ITERATION"))
987
+ p_curve = gr.HTML()
988
+ gr.HTML(rule("ITERATION LOG"))
989
+ p_log = gr.HTML()
 
 
 
990
  gr.HTML(rule("LEARNED POLICY CELL"))
991
  p_policy = gr.HTML()
 
 
992
  # compact secondary zone (~20%): log a REAL print back into the ledger
993
  with gr.Row(elem_classes=["ce-card"]):
994
  with gr.Column(scale=1):
 
1024
  # ── wiring ──
1025
  reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
1026
  p_log, p_headline, outcome_panel, results_group, real_log_msg,
1027
+ studio_log, part, model3d, part_placeholder, part_status,
1028
+ build_results, read_results, tabs)
1029
  preview_outs = [part, model3d, part_status, part_placeholder]
1030
  foot_in = [part, material, temp, humidity, bed_position]
1031
  benchy_btn.click(load_benchy, None, preview_outs).then(status_footer, foot_in, [footer])
 
1051
 
1052
  # Two-step BUILD: instant loader + tab-switch, then the heavy model call (reveals
1053
  # the results), then refresh the inherited-job readout on the Print tab.
1054
+ build_start_outs = [tabs, build_results, read_results, read_loader, vprint, vp_slider,
1055
+ vp_layer, to_print_btn, override_btn, second_opinion_panel,
1056
+ read_toggle, eng_read_group, second_op_group]
1057
  build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
1058
  gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,
1059
+ read_loader]
1060
  run_btn.click(build_start, [part], build_start_outs).then(
1061
  build_job, [part, material, description, temp, humidity, bed_position, model_select],
1062
  build_outs).then(job_readout, [state], [p_job])
 
1064
  vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
1065
  read_toggle.change(toggle_read, [read_toggle, state],
1066
  [eng_read_group, second_op_group, second_opinion_panel,
1067
+ to_print_btn, override_btn, state])
1068
  override_btn.click(ack_override, state, [to_print_btn, override_btn])
1069
  to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
1070
  None, None, None, js=_SCROLL_TOP)
1071
+ tabs.select(refresh_print_tab, [state], [p_job, plan_settings, plan_verdict])
1072
 
1073
  # PRINT: run the loop on THIS job (slider 1 = single print), or log a real outcome.
1074
+ print_outs = [results_group, outcome_panel, p_headline, p_curve, p_log, p_policy,
1075
  ledger_panel, node_cards, review_summary]
1076
+ p_run.click(lambda: gr.update(visible=False), None, [vary_group]).then(
1077
+ run_print, [state, p_iters, p_nozzle, p_bed, p_fan, p_retract], print_outs).then(
1078
  None, None, None, js=_SCROLL_TOP)
1079
+ p_iters.change(lambda v: f"<div class='ce-sub' style='text-align:center;'><b>{int(v)}</b> ITERATIONS</div>",
1080
+ [p_iters], [p_iter_readout])
1081
+ vary_btn.click(open_vary_panel, [state], [vary_group, p_nozzle, p_bed, p_fan, p_retract])
1082
+ reset_plan_btn.click(open_vary_panel, [state], [vary_group, p_nozzle, p_bed, p_fan, p_retract])
1083
  for btn, oc in [(b_clean, "success"), (b_sag, "failed_sag"), (b_string, "failed_stringing")]:
1084
  btn.click(record_outcome, [gr.State(oc), state], [real_log_msg, ledger_panel, node_cards])
1085
 
core/chief_engineer.py CHANGED
@@ -16,10 +16,10 @@ from .prompts import build_system_prompt
16
 
17
  # Conservative per-material starting points for the offline/parse-fail fallback.
18
  _FALLBACK_SETTINGS: dict[str, dict[str, float]] = {
19
- "PLA": {"nozzle_temp": 205, "bed_temp": 60, "retraction_mm": 5, "fan_pct": 100, "first_layer_fan_pct": 0},
20
- "PETG": {"nozzle_temp": 235, "bed_temp": 80, "retraction_mm": 4, "fan_pct": 40, "first_layer_fan_pct": 0},
21
- "ABS": {"nozzle_temp": 245, "bed_temp": 100, "retraction_mm": 4, "fan_pct": 20, "first_layer_fan_pct": 0},
22
- "TPU": {"nozzle_temp": 228, "bed_temp": 50, "retraction_mm": 2, "fan_pct": 40, "first_layer_fan_pct": 0},
23
  }
24
  _GEOMETRY_RISK = {
25
  "overhang": ("underside of the overhang", "sag", "steep overhangs droop before the layer freezes", "overhang"),
 
16
 
17
  # Conservative per-material starting points for the offline/parse-fail fallback.
18
  _FALLBACK_SETTINGS: dict[str, dict[str, float]] = {
19
+ "PLA": {"nozzle_temp": 205, "bed_temp": 60, "retraction_mm": 5, "fan_pct": 100, "first_layer_fan_pct": 0, "layer_height": 0.20},
20
+ "PETG": {"nozzle_temp": 235, "bed_temp": 80, "retraction_mm": 4, "fan_pct": 40, "first_layer_fan_pct": 0, "layer_height": 0.20},
21
+ "ABS": {"nozzle_temp": 245, "bed_temp": 100, "retraction_mm": 4, "fan_pct": 20, "first_layer_fan_pct": 0, "layer_height": 0.20},
22
+ "TPU": {"nozzle_temp": 228, "bed_temp": 50, "retraction_mm": 2, "fan_pct": 40, "first_layer_fan_pct": 0, "layer_height": 0.24},
23
  }
24
  _GEOMETRY_RISK = {
25
  "overhang": ("underside of the overhang", "sag", "steep overhangs droop before the layer freezes", "overhang"),
core/models.py CHANGED
@@ -37,6 +37,7 @@ class PrintSettings(BaseModel):
37
  retraction_mm: float
38
  fan_pct: float
39
  first_layer_fan_pct: float
 
40
 
41
 
42
  class RiskRegion(BaseModel):
 
37
  retraction_mm: float
38
  fan_pct: float
39
  first_layer_fan_pct: float
40
+ layer_height: float = 0.2 # mm β€” drives virtual-print preview cross-sections
41
 
42
 
43
  class RiskRegion(BaseModel):
core/prompts.py CHANGED
@@ -27,7 +27,7 @@ OUTPUT_CONTRACT = """Respond ONLY with valid JSON, no prose outside it, in exact
27
  "reasoning": "2-4 sentences. START with your evaluation of the prior jobs: what transfers, what doesn't, and why. Then the decision.",
28
  "settings": {
29
  "nozzle_temp": <C>, "bed_temp": <C>, "retraction_mm": <mm>,
30
- "fan_pct": <0-100>, "first_layer_fan_pct": <0-100>
31
  },
32
  "risks": [
33
  {"location": "where on the part", "risk": "sag|stringing|adhesion|warping|delamination",
 
27
  "reasoning": "2-4 sentences. START with your evaluation of the prior jobs: what transfers, what doesn't, and why. Then the decision.",
28
  "settings": {
29
  "nozzle_temp": <C>, "bed_temp": <C>, "retraction_mm": <mm>,
30
+ "fan_pct": <0-100>, "first_layer_fan_pct": <0-100>, "layer_height": <mm, e.g. 0.12-0.28>
31
  },
32
  "risks": [
33
  {"location": "where on the part", "risk": "sag|stringing|adhesion|warping|delamination",
core/spine.py CHANGED
@@ -23,6 +23,7 @@ COMMON_BOUNDS: dict[str, tuple[float, float]] = {
23
  "retraction_mm": (0.5, 8.0),
24
  "fan_pct": (0.0, 100.0),
25
  "first_layer_fan_pct": (0.0, 30.0),
 
26
  }
27
 
28
 
 
23
  "retraction_mm": (0.5, 8.0),
24
  "fan_pct": (0.0, 100.0),
25
  "first_layer_fan_pct": (0.0, 30.0),
26
+ "layer_height": (0.08, 0.60),
27
  }
28
 
29
 
core/theme.py CHANGED
@@ -202,31 +202,20 @@ textarea:focus, input:focus {{ border-color:var(--ao-orange) !important; box-sha
202
  .ce-icon-arrow::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); }}
203
 
204
  /* Arrow AFTER text (right-side icon β€” indicates "next step"). For buttons whose
205
- label means "proceed to next stage" (SLICE β†’, PRINT β†’, REFRESH β†’). */
206
- .ce-icon-arrow-after::after {{
207
- content:""; display:inline-block; width:14px; height:14px;
208
- margin-left:8px; vertical-align:-2px;
209
- background:currentColor; -webkit-mask-size:contain; mask-size:contain;
210
- -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
211
- -webkit-mask-position:center; mask-position:center;
212
- -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
213
- mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
214
- }}
215
- /* Inside a Gradio button the label is nested in a <span> β€” attach to button so
216
- the ::after lands at the end of the visible label, not after the wrapper. */
217
- .ce-icon-arrow-after button::after, button.ce-icon-arrow-after::after {{
218
- content:""; display:inline-block; width:14px; height:14px;
219
- margin-left:8px; vertical-align:-2px;
220
  background:currentColor; -webkit-mask-size:contain; mask-size:contain;
221
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
222
  -webkit-mask-position:center; mask-position:center;
223
  -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
224
  mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
225
  }}
226
- /* But ALSO put one inside the inner span so it appears flush with the label text */
227
- .ce-icon-arrow-after button span::after {{
228
- content:"\2192"; margin-left:6px; font-weight:900; opacity:.95;
229
- }}
230
 
231
  /* ── TAB ICONS (CSS-only via :nth-child on the four workflow tabs) ── */
232
  /* Tab bar order is fixed: LOAD Β· SLICE Β· PRINT Β· REVIEW. Each gets its own icon
@@ -585,6 +574,48 @@ CSS += """
585
  /* model switcher pills sit in the header row β€” KEEP concise, don't conflict with main rule */
586
  #ce-modelswitch .ce-seg label {{ font-size:11px !important; padding:5px 12px !important; }}
587
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  /* hover callout (info icon next to the model switcher) */
589
  .ce-callout { position:relative; display:inline-flex; align-items:center; gap:4px; cursor:help;
590
  color:var(--ao-orange); font-size:11px; letter-spacing:1px; text-transform:uppercase; }
@@ -716,6 +747,8 @@ _ICONS: dict[str, tuple[str, bool]] = {
716
  "thermo": ("M14 14V5a2 2 0 10-4 0v9a4 4 0 104 0z", False),
717
  "droplet": ("M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z", False),
718
  "target": ("M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z", False),
 
 
719
  "printer": ("M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 "
720
  "M6 14h12v8H6z", False),
721
  "play": ("M6 4l14 8-14 8V4z", True),
 
202
  .ce-icon-arrow::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); }}
203
 
204
  /* Arrow AFTER text (right-side icon β€” indicates "next step"). For buttons whose
205
+ label means "proceed to next stage" (SLICE β†’, PRINT β†’, REFRESH β†’).
206
+ Gradio wraps each button in a .wrap; elem_classes lands on that wrapper, so we
207
+ target the label <span> inside the <button> and append the arrow as an SVG
208
+ mask-image pseudo-element. */
209
+ .ce-icon-arrow-after button {{ display:inline-flex !important; align-items:center !important; gap:4px !important; }}
210
+ .ce-icon-arrow-after button span::after {{
211
+ content:""; display:inline-block; width:14px; height:14px; flex:0 0 14px;
212
+ margin-left:4px; vertical-align:-2px;
 
 
 
 
 
 
 
213
  background:currentColor; -webkit-mask-size:contain; mask-size:contain;
214
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
215
  -webkit-mask-position:center; mask-position:center;
216
  -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
217
  mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
218
  }}
 
 
 
 
219
 
220
  /* ── TAB ICONS (CSS-only via :nth-child on the four workflow tabs) ── */
221
  /* Tab bar order is fixed: LOAD Β· SLICE Β· PRINT Β· REVIEW. Each gets its own icon
 
574
  /* model switcher pills sit in the header row β€” KEEP concise, don't conflict with main rule */
575
  #ce-modelswitch .ce-seg label {{ font-size:11px !important; padding:5px 12px !important; }}
576
 
577
+ /* compact inline pill groups inside the envbar (BUILD-PLATE POSITION + MATERIAL) */
578
+ .ce-envbar .ce-inline-group { display:flex !important; flex-direction:column !important;
579
+ gap:3px !important; margin:0 10px !important; }
580
+ .ce-envbar .ce-inline-label { color:var(--ao-orange); font-size:10px !important;
581
+ letter-spacing:1.5px; text-transform:uppercase; font-weight:700;
582
+ display:flex !important; align-items:center !important; gap:4px; }
583
+ .ce-envbar .ce-inline-label .ce-callout { font-size:10px !important; }
584
+ .ce-envbar .ce-inline-label .ce-tip { width:280px; top:120%; }
585
+ /* inline pills are smaller than the full-width ones */
586
+ .ce-envbar .ce-pills label { padding:4px 10px !important; font-size:11px !important; }
587
+ .ce-envbar .ce-pills label span { display:inline-flex !important; align-items:center !important;
588
+ gap:5px !important; }
589
+ /* pill icons by value (small, before the label text) */
590
+ .ce-pills label:has(input[value="center"]) span::before,
591
+ .ce-pills label:has(input[value="edge"]) span::before,
592
+ .ce-pills label:has(input[value="corner"]) span::before,
593
+ .ce-pills label:has(input[value="PLA"]) span::before,
594
+ .ce-pills label:has(input[value="PETG"]) span::before,
595
+ .ce-pills label:has(input[value="ABS"]) span::before,
596
+ .ce-pills label:has(input[value="TPU"]) span::before {
597
+ content:""; display:inline-block; width:11px; height:11px; flex:0 0 11px;
598
+ background:currentColor; -webkit-mask-size:contain; mask-size:contain;
599
+ -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
600
+ -webkit-mask-position:center; mask-position:center; }
601
+ .ce-pills label:has(input[value="center"]) span::before {
602
+ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z'/></svg>");
603
+ mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z'/></svg>"); }
604
+ .ce-pills label:has(input[value="edge"]) span::before {
605
+ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v16'/></svg>");
606
+ mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v16'/></svg>"); }
607
+ .ce-pills label:has(input[value="corner"]) span::before {
608
+ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v7 M4 4h7'/></svg>");
609
+ mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 4h16v16H4z M4 4v7 M4 4h7'/></svg>"); }
610
+ .ce-pills label:has(input[value="PLA"]) span::before,
611
+ .ce-pills label:has(input[value="PETG"]) span::before,
612
+ .ce-pills label:has(input[value="ABS"]) span::before,
613
+ .ce-pills label:has(input[value="TPU"]) span::before {
614
+ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z'/></svg>");
615
+ mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z'/></svg>"); }
616
+ /* tighten space below the envbar so the PART card sits directly under it */
617
+ .ce-envbar { margin-bottom:2px !important; }
618
+
619
  /* hover callout (info icon next to the model switcher) */
620
  .ce-callout { position:relative; display:inline-flex; align-items:center; gap:4px; cursor:help;
621
  color:var(--ao-orange); font-size:11px; letter-spacing:1px; text-transform:uppercase; }
 
747
  "thermo": ("M14 14V5a2 2 0 10-4 0v9a4 4 0 104 0z", False),
748
  "droplet": ("M12 2.5C12 2.5 5 10 5 14a7 7 0 0014 0c0-4-7-11.5-7-11.5z", False),
749
  "target": ("M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 7a5 5 0 100 10 5 5 0 000-10z", False),
750
+ "edge": ("M4 4h16v16H4z M4 4v16", False),
751
+ "corner": ("M4 4h16v16H4z M4 4v7 M4 4h7", False),
752
  "printer": ("M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 "
753
  "M6 14h12v8H6z", False),
754
  "play": ("M6 4l14 8-14 8V4z", True),
core/viewer.py CHANGED
@@ -288,6 +288,7 @@ def gcode_readout(settings: PrintSettings, material: str) -> str:
288
  """Short snippet whose header lines come from the proposed settings."""
289
  return "\n".join([
290
  f"; Chief Engineer β€” start g-code for {material} (header tied to recommendation)",
 
291
  f"M140 S{settings.bed_temp:.0f} ; set bed",
292
  f"M104 S{settings.nozzle_temp:.0f} ; set nozzle",
293
  f"M190 S{settings.bed_temp:.0f} ; wait for bed",
@@ -328,9 +329,10 @@ def quality_curve_html(trajectory: list[float], threshold: float = 0.7) -> str:
328
  )
329
 
330
 
331
- def iteration_log_html(records, verdicts=None) -> str:
332
  """Per-iteration log. `verdicts` (optional, aligned to records) adds the QA
333
- Inspector's terse grade on each run β€” the second voice in the loop."""
 
334
  rows = []
335
  for i, r in enumerate(records):
336
  clean = r.result.outcome == "success"
@@ -339,6 +341,10 @@ def iteration_log_html(records, verdicts=None) -> str:
339
  if verdicts and i < len(verdicts) and verdicts[i] is not None:
340
  v = verdicts[i]
341
  insp = (f"<br><span style='color:{v.color};'>{icon('search')} inspector [{v.stance}]: {v.headline}</span>")
 
 
 
 
342
  rows.append(
343
  f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
344
  f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
@@ -346,7 +352,7 @@ def iteration_log_html(records, verdicts=None) -> str:
346
  f"noz {r.settings.nozzle_temp:.0f}Β°C bed {r.settings.bed_temp:.0f}Β°C fan {r.settings.fan_pct:.0f}% "
347
  f"ret {r.settings.retraction_mm:.1f}mm"
348
  + (" Β· <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
349
- + f"<br><span style='color:var(--ao-outline);'>↳ {r.learned}</span>{insp}</div>"
350
  )
351
  return "".join(rows)
352
 
 
288
  """Short snippet whose header lines come from the proposed settings."""
289
  return "\n".join([
290
  f"; Chief Engineer β€” start g-code for {material} (header tied to recommendation)",
291
+ f"; layer height {settings.layer_height:.2f} mm",
292
  f"M140 S{settings.bed_temp:.0f} ; set bed",
293
  f"M104 S{settings.nozzle_temp:.0f} ; set nozzle",
294
  f"M190 S{settings.bed_temp:.0f} ; wait for bed",
 
329
  )
330
 
331
 
332
+ def iteration_log_html(records, verdicts=None, timings=None) -> str:
333
  """Per-iteration log. `verdicts` (optional, aligned to records) adds the QA
334
+ Inspector's terse grade on each run β€” the second voice in the loop.
335
+ `timings` (optional, aligned to records) adds per-iteration elapsed ms."""
336
  rows = []
337
  for i, r in enumerate(records):
338
  clean = r.result.outcome == "success"
 
341
  if verdicts and i < len(verdicts) and verdicts[i] is not None:
342
  v = verdicts[i]
343
  insp = (f"<br><span style='color:{v.color};'>{icon('search')} inspector [{v.stance}]: {v.headline}</span>")
344
+ timing = ""
345
+ if timings and i < len(timings):
346
+ timing = (f"<span style='float:right;color:var(--ao-outline);'>"
347
+ f"+{timings[i]*1000:.0f}ms</span>")
348
  rows.append(
349
  f"<div style='font-family:ui-monospace,monospace;font-size:11px;border-left:3px solid {col};"
350
  f"background:var(--ao-surface);padding:5px 10px;margin:3px 0;color:var(--ao-text);'>"
 
352
  f"noz {r.settings.nozzle_temp:.0f}Β°C bed {r.settings.bed_temp:.0f}Β°C fan {r.settings.fan_pct:.0f}% "
353
  f"ret {r.settings.retraction_mm:.1f}mm"
354
  + (" Β· <span style='color:var(--ao-orange);'>spine-clamped</span>" if r.clamped else "")
355
+ + f"{timing}<br><span style='color:var(--ao-outline);'>↳ {r.learned}</span>{insp}</div>"
356
  )
357
  return "".join(rows)
358
 
core/widgets.py CHANGED
@@ -20,7 +20,7 @@ from pathlib import Path
20
  _ISO = 0.55 # oblique projection skew, matches sim/virtual_printer
21
 
22
 
23
- def _projected_layers(mesh_path: str | Path, layer_height_mm: float = 0.8,
24
  max_layers: int = 44, max_segs_per_layer: int = 520) -> dict:
25
  """Slice the mesh and return projected 2D polylines per layer + bounds.
26
 
@@ -134,22 +134,30 @@ def layer_image(mesh_path: str | Path | None, idx: int, n: int = SCRUB_LAYERS,
134
  return np.asarray(img)
135
 
136
 
137
- def virtual_printer_html(mesh_path: str | Path | None, caption: str = "") -> str:
138
- """A live virtual-printer canvas for the cockpit (animated by VP_JS)."""
 
 
 
 
 
 
139
  if not mesh_path or not Path(str(mesh_path)).exists():
140
  return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
141
  "<div class='ce-sub'>run a recommendation to slice the part β†’</div>")
142
- data = _projected_layers(mesh_path)
 
143
  if not data:
144
  return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
145
  "<div class='ce-sub'>mesh too thin to slice.</div>")
146
  payload = json.dumps(data).replace("'", "&#39;")
 
147
  return (
148
  "<div class='ce-rule'>VIRTUAL PRINT</div>"
149
  "<div style='border:1px solid var(--ao-outline-dim);background:#0a0c14;'>"
150
  f"<canvas class='ce-vp' width='520' height='300' data-vp='{payload}' "
151
  "style='width:100%;height:auto;display:block;'></canvas></div>"
152
- f"<div class='ce-sub' style='margin-top:2px;'>{caption} Β· real cross-sections of "
153
  "this part, layer by layer (motion preview β€” not a slicer)</div>"
154
  )
155
 
 
20
  _ISO = 0.55 # oblique projection skew, matches sim/virtual_printer
21
 
22
 
23
+ def _projected_layers(mesh_path: str | Path, layer_height_mm: float = 0.2,
24
  max_layers: int = 44, max_segs_per_layer: int = 520) -> dict:
25
  """Slice the mesh and return projected 2D polylines per layer + bounds.
26
 
 
134
  return np.asarray(img)
135
 
136
 
137
+ def virtual_printer_html(mesh_path: str | Path | None,
138
+ settings: "PrintSettings | None" = None,
139
+ caption: str = "") -> str:
140
+ """A live virtual-printer canvas for the cockpit (animated by VP_JS).
141
+
142
+ Uses the layer height from the proposed PrintSettings so the preview is tied
143
+ to the actual recommendation rather than a hard-coded default.
144
+ """
145
  if not mesh_path or not Path(str(mesh_path)).exists():
146
  return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
147
  "<div class='ce-sub'>run a recommendation to slice the part β†’</div>")
148
+ layer_height = settings.layer_height if settings else 0.2
149
+ data = _projected_layers(mesh_path, layer_height_mm=layer_height)
150
  if not data:
151
  return ("<div class='ce-rule'>VIRTUAL PRINT</div>"
152
  "<div class='ce-sub'>mesh too thin to slice.</div>")
153
  payload = json.dumps(data).replace("'", "&#39;")
154
+ lh_note = f"{layer_height:.2f} mm layers"
155
  return (
156
  "<div class='ce-rule'>VIRTUAL PRINT</div>"
157
  "<div style='border:1px solid var(--ao-outline-dim);background:#0a0c14;'>"
158
  f"<canvas class='ce-vp' width='520' height='300' data-vp='{payload}' "
159
  "style='width:100%;height:auto;display:block;'></canvas></div>"
160
+ f"<div class='ce-sub' style='margin-top:2px;'>{caption} Β· {lh_note} Β· real cross-sections of "
161
  "this part, layer by layer (motion preview β€” not a slicer)</div>"
162
  )
163
 
dist/deliberation/deliberations.jsonl CHANGED
@@ -1,29 +1,29 @@
1
- {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists, so I reason from ABS properties and the current environment. ABS is prone to warping and requires high thermal management; 26Β°C ambient is acceptable but not ideal. The overhang geometry demands careful temperature control to prevent sag or delamination. We must set temps aggressively to keep the plastic fluid enough for the overhang while minimizing residual stress.\nProposed: nozzle 255Β°C, bed 100Β°C, fan 30%, first-layer fan 0%, retraction 4.0mm.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:07+00:00"}
2
  {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:14+00:00"}
3
- {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "dispute", "content": "Temperatures are too aggressive and unvalidated. β€” The proposed high temps increase warping risk significantly, especially with unknown thermal chamber performance.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:21+00:00"}
4
- {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 4, "agent": "Operator", "role": "Operator", "act": "override", "stance": "override", "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:28+00:00"}
5
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 5, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 1: sag (quality 0.19, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.19) β€” learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:35+00:00"}
6
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 6, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β€” exactly as called. β€” Quality 0.19. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:42+00:00"}
7
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 7, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 2: sag (quality 0.27, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.27) β€” learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:49+00:00"}
8
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 8, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β€” exactly as called. β€” Quality 0.27. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:56+00:00"}
9
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 9, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 3: sag (quality 0.33, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.33) β€” learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:03+00:00"}
10
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β€” exactly as called. β€” Quality 0.33. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:10+00:00"}
11
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 11, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 4: warp (quality 0.34, dominant cost: warp). Policy: ABS/overhang/mid/humid: warp (q=0.34) β€” learned: fan_pct -10.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:17+00:00"}
12
- {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 12, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "missed", "content": "Failed on warping β€” and it wasn't flagged. β€” Quality 0.34. The Engineer didn't predict this mode. That gap is what the next iteration has to close.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:24+00:00"}
13
- {"session_id": "abs-overhang-edge", "track": "review", "turn": 13, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "caution", "content": "Improving but not yet clean (best 0.34). β€” Quality rose 0.19 β†’ 0.34 over 4 runs but never crossed the bar. More iterations or a different lever needed β€” the loop is learning, slowly.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:31+00:00"}
14
- {"session_id": "petg-overhang-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The prior job [1] is the closest precedent, involving PETG and an overhang. The lesson learnedβ€”that PETG needs a cooling compromiseβ€”is critical here. I will maintain the fan settings from that job's failure analysis (60-70% over overhang) because it directly addresses the material/geometry interaction. Since the environment (24Β°C, 55% RH) is very close to the precedent's conditions, the core temperature and cooling strategy remains valid.\nProposed: nozzle 245Β°C, bed 85Β°C, fan 65%, first-layer fan 0%, retraction 5.0mm.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:38+00:00"}
15
- {"session_id": "petg-overhang-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:45+00:00"}
16
- {"session_id": "petg-overhang-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Cooling compromise ignores thermal gradient risk. β€” The 65% fan setting risks rapid cooling and warping on PETG overhangs, especially if local drafts exceed ambient control.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:52+00:00"}
17
- {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β€” holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:59+00:00"}
18
- {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β€” Engineer flagged sag; the settings covered it. Credit the call β€” or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:06+00:00"}
19
- {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β€” holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:13+00:00"}
20
- {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β€” Engineer flagged sag; the settings covered it. Credit the call β€” or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:20+00:00"}
21
- {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β€” holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:27+00:00"}
22
- {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β€” Engineer flagged sag; the settings covered it. Credit the call β€” or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:34+00:00"}
23
- {"session_id": "petg-overhang-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β€” PETG/overhang climbed 0.73 β†’ 0.73. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:41+00:00"}
24
- {"session_id": "pla-adhesion-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The closest precedent [1] shows that cold ambient temperature (19Β°C) severely compromised first-layer adhesion, requiring a significant bed bump and fan shutdown. Since the current room is 21Β°C, which is only slightly warmer than the failed job's environment, we must apply the core lesson: prioritize thermal energy transfer to the build plate. We will maintain the elevated bed temperature and disable cooling for initial layers to maximize bonding strength.\nProposed: nozzle 215Β°C, bed 60Β°C, fan 0%, first-layer fan 0%, retraction 5.0mm.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:48+00:00"}
25
  {"session_id": "pla-adhesion-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:55+00:00"}
26
- {"session_id": "pla-adhesion-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Thermal gradient assumption is weak. β€” A slight ambient bump does not guarantee adhesion success. The print environment must be stable, not just marginally improved.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:02+00:00"}
27
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β€” holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:09+00:00"}
28
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β€” Engineer flagged adhesion; the settings covered it. Credit the call β€” or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:16+00:00"}
29
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β€” holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:23+00:00"}
@@ -31,14 +31,13 @@
31
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β€” holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:37+00:00"}
32
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β€” Engineer flagged adhesion; the settings covered it. Credit the call β€” or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:44+00:00"}
33
  {"session_id": "pla-adhesion-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β€” PLA/adhesion climbed 1.00 β†’ 1.00. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:51+00:00"}
34
- {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists. TPU is flexible and prone to stringing because of its high elasticity and low melt strength, especially when ambient conditions are humid (65% RH). The goal is minimizing oozing without compromising print speed or layer adhesion. We need a balance: enough heat to flow the material but not so much that it overheats and degrades, and aggressive cooling where possible.\nProposed: nozzle 230Β°C, bed 45Β°C, fan 60%, first-layer fan 0%, retraction 1.5mm.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:03:58+00:00"}
35
  {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:05+00:00"}
36
- {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "dispute", "content": "High heat settings for TPU are risky overkill. β€” 230Β°C nozzle risks degradation and excessive ooze. Lower it and manage cooling/retraction better.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:12+00:00"}
37
- {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 4, "agent": "Operator", "role": "Operator", "act": "override", "stance": "override", "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:19+00:00"}
38
- {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 5, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β€” holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:26+00:00"}
39
- {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 6, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β€” Engineer flagged stringing; the settings covered it. Credit the call β€” or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:33+00:00"}
40
- {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 7, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β€” holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:40+00:00"}
41
- {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 8, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β€” Engineer flagged stringing; the settings covered it. Credit the call β€” or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:47+00:00"}
42
- {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 9, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β€” holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:54+00:00"}
43
- {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β€” Engineer flagged stringing; the settings covered it. Credit the call β€” or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:01+00:00"}
44
- {"session_id": "tpu-stringing-edge", "track": "review", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β€” TPU/stringing climbed 0.86 β†’ 0.86. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:08+00:00"}
 
1
+ {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists. ABS is high warp material, and overhangs amplify this risk significantly. The room temp (26Β°C) is moderate but requires active thermal management to prevent warping during cooling. We must maintain a stable thermal gradient from the bed up through the print area. I will use established ABS parameters for adhesion and minimize forced air.\nProposed: nozzle 255Β°C, bed 100Β°C, fan 30%, first-layer fan 0%, retraction 4.0mm.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:07+00:00"}
2
  {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:14+00:00"}
3
+ {"session_id": "abs-overhang-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Thermal management remains a guess, not a plan. β€” The reliance on ambient room temp and moderate bed temps doesn't guarantee stability. Warping risk is unflagged until it happens.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:21+00:00"}
4
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 1: sag (quality 0.19, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.19) β€” learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:28+00:00"}
5
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β€” exactly as called. β€” Quality 0.19. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:35+00:00"}
6
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 2: sag (quality 0.27, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.27) β€” learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:42+00:00"}
7
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β€” exactly as called. β€” Quality 0.27. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:49+00:00"}
8
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 3: sag (quality 0.33, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.33) β€” learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:56+00:00"}
9
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β€” exactly as called. β€” Quality 0.33. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:03+00:00"}
10
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 10, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 4: warp (quality 0.34, dominant cost: warp). Policy: ABS/overhang/mid/humid: warp (q=0.34) β€” learned: fan_pct -10.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:10+00:00"}
11
+ {"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "missed", "content": "Failed on warping β€” and it wasn't flagged. β€” Quality 0.34. The Engineer didn't predict this mode. That gap is what the next iteration has to close.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:17+00:00"}
12
+ {"session_id": "abs-overhang-edge", "track": "review", "turn": 12, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "caution", "content": "Improving but not yet clean (best 0.34). β€” Quality rose 0.19 β†’ 0.34 over 4 runs but never crossed the bar. More iterations or a different lever needed β€” the loop is learning, slowly.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:24+00:00"}
13
+ {"session_id": "petg-overhang-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The prior job [1] is highly relevant, showing PETG overhang sag at 23Β°C. Since the current environment (24Β°C, 55% RH) is only slightly warmer and drier, the lesson about cooling compromise remains critical. I must maintain the fan setting high enough to stabilize the plastic but low enough not to induce thermal stress or excessive warping on PETG. The overhang geometry dictates careful management of cooling rates.\nProposed: nozzle 245Β°C, bed 85Β°C, fan 60%, first-layer fan 0%, retraction 5.0mm.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:31+00:00"}
14
+ {"session_id": "petg-overhang-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:38+00:00"}
15
+ {"session_id": "petg-overhang-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "dispute", "content": "Overhang sag and environment are not solved by minor temperature tweaks. β€” The proposed fan settings fail to account for PETG's inherent thermal instability. Adhesion risk remains high regardless of the slight environmental shift.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:45+00:00"}
16
+ {"session_id": "petg-overhang-center", "track": "preflight", "turn": 4, "agent": "Operator", "role": "Operator", "act": "override", "stance": "override", "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:52+00:00"}
17
+ {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 5, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β€” holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:59+00:00"}
18
+ {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 6, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β€” Engineer flagged sag; the settings covered it. Credit the call β€” or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:06+00:00"}
19
+ {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 7, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β€” holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:13+00:00"}
20
+ {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 8, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β€” Engineer flagged sag; the settings covered it. Credit the call β€” or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:20+00:00"}
21
+ {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 9, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β€” holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:27+00:00"}
22
+ {"session_id": "petg-overhang-center", "track": "print-loop", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β€” Engineer flagged sag; the settings covered it. Credit the call β€” or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:34+00:00"}
23
+ {"session_id": "petg-overhang-center", "track": "review", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β€” PETG/overhang climbed 0.73 β†’ 0.73. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:41+00:00"}
24
+ {"session_id": "pla-adhesion-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The prior job [1] (PLA/adhesion @ 19Β°C, 38% RH) is highly relevant because the current ambient temperature (21Β°C) is still cool relative to optimal printing conditions, and adhesion failure was the primary issue. The lesson that cold ambient air compromises first-layer bonding remains valid. Therefore, we must maintain a high bed temp and minimize cooling effects on the first few layers.\nProposed: nozzle 215Β°C, bed 60Β°C, fan 30%, first-layer fan 0%, retraction 5.0mm.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:48+00:00"}
25
  {"session_id": "pla-adhesion-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:55+00:00"}
26
+ {"session_id": "pla-adhesion-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "The ambient conditions are not a guarantee. β€” Relying on past environmental data to predict adhesion success is thin. Watch for drafts or humidity spikes.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:02+00:00"}
27
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β€” holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:09+00:00"}
28
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β€” Engineer flagged adhesion; the settings covered it. Credit the call β€” or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:16+00:00"}
29
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β€” holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:23+00:00"}
 
31
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β€” holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:37+00:00"}
32
  {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β€” Engineer flagged adhesion; the settings covered it. Credit the call β€” or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:44+00:00"}
33
  {"session_id": "pla-adhesion-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β€” PLA/adhesion climbed 1.00 β†’ 1.00. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:51+00:00"}
34
+ {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists (TPU/stringing). TPU is flexible and prone to stringing due to its viscoelastic nature, especially when printed at ambient temperatures. The high humidity (65% RH) will exacerbate this by increasing surface tackiness. We must minimize the time the plastic spends in the air after extrusion while maintaining flow. I'll keep the nozzle temp low but stable, increase retraction slightly for better cleanup, and use a moderate fan to help cool the stringing areas quickly.\nProposed: nozzle 230Β°C, bed 45Β°C, fan 60%, first-layer fan 0%, retraction 3.0mm.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:03:58+00:00"}
35
  {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β€” no clamp.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:05+00:00"}
36
+ {"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Humidity risk outweighs temperature adjustments. β€” 65% RH and TPU are a poor pairing. Focus on enclosure or drying, not just retraction settings.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:12+00:00"}
37
+ {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β€” holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:19+00:00"}
38
+ {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β€” Engineer flagged stringing; the settings covered it. Credit the call β€” or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:26+00:00"}
39
+ {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β€” holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:33+00:00"}
40
+ {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β€” Engineer flagged stringing; the settings covered it. Credit the call β€” or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:40+00:00"}
41
+ {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β€” holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:47+00:00"}
42
+ {"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β€” Engineer flagged stringing; the settings covered it. Credit the call β€” or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:54+00:00"}
43
+ {"session_id": "tpu-stringing-edge", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β€” TPU/stringing climbed 0.86 β†’ 0.86. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:01+00:00"}
 
learn/loop.py CHANGED
@@ -67,8 +67,8 @@ def _record_lesson(job: Job, env: Environment, s: PrintSettings, result: SimResu
67
 
68
 
69
  def run_iteration(job: Job, env: Environment, policy: LearnedPolicy, ledger: LedgerManager,
70
- n: int, record: bool = True) -> IterationRecord:
71
- proposed = policy.propose(job.material, job.geometry_type, env)
72
  checked = _SPINE.check(proposed, job.material)
73
  settings = checked.settings
74
  result = simulate(settings, job, env)
 
67
 
68
 
69
  def run_iteration(job: Job, env: Environment, policy: LearnedPolicy, ledger: LedgerManager,
70
+ n: int, record: bool = True, overrides: PrintSettings | None = None) -> IterationRecord:
71
+ proposed = overrides if overrides is not None else policy.propose(job.material, job.geometry_type, env)
72
  checked = _SPINE.check(proposed, job.material)
73
  settings = checked.settings
74
  result = simulate(settings, job, env)
test_core.py CHANGED
@@ -86,9 +86,27 @@ def test_gcode_readout_ties_to_settings():
86
  s = PrintSettings(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)
87
  g = gcode_readout(s, "PLA")
88
  assert "M104 S205" in g and "M140 S60" in g, g
 
89
  print("βœ“ g-code header is populated from proposed settings")
90
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  def test_ingest_distiller():
93
  from pathlib import Path as _P
94
  from ingest.distill import parse_prusa_ini, parse_klipper_cfg, parse_marlin_config
@@ -163,6 +181,7 @@ if __name__ == "__main__":
163
  test_reflect_appends()
164
  test_retrieval_orders_by_env_distance()
165
  test_gcode_readout_ties_to_settings()
 
166
  test_ingest_distiller()
167
  test_precedent_eval_narration()
168
  test_simulator_physical_and_deterministic()
 
86
  s = PrintSettings(nozzle_temp=205, bed_temp=60, retraction_mm=5, fan_pct=100, first_layer_fan_pct=0)
87
  g = gcode_readout(s, "PLA")
88
  assert "M104 S205" in g and "M140 S60" in g, g
89
+ assert "layer height 0.20 mm" in g, g
90
  print("βœ“ g-code header is populated from proposed settings")
91
 
92
 
93
+ # GIF export button removed; keep motion preview tests via UI smoke if needed.
94
+
95
+
96
+ def test_virtual_printer_html_ties_to_settings():
97
+ from core.widgets import virtual_printer_html
98
+ from core.viewer import generate_primitive
99
+ from core.models import PrintSettings
100
+ mesh, _geo = generate_primitive("box", 20)
101
+ default_html = virtual_printer_html(mesh)
102
+ assert "0.20 mm layers" in default_html, default_html
103
+ fine = PrintSettings(nozzle_temp=200, bed_temp=60, retraction_mm=4.5, fan_pct=80,
104
+ first_layer_fan_pct=0, layer_height=0.12)
105
+ fine_html = virtual_printer_html(mesh, settings=fine)
106
+ assert "0.12 mm layers" in fine_html, fine_html
107
+ print("βœ“ virtual-print preview layer height follows PrintSettings")
108
+
109
+
110
  def test_ingest_distiller():
111
  from pathlib import Path as _P
112
  from ingest.distill import parse_prusa_ini, parse_klipper_cfg, parse_marlin_config
 
181
  test_reflect_appends()
182
  test_retrieval_orders_by_env_distance()
183
  test_gcode_readout_ties_to_settings()
184
+ test_virtual_printer_html_ties_to_settings()
185
  test_ingest_distiller()
186
  test_precedent_eval_narration()
187
  test_simulator_physical_and_deterministic()