kylebrodeur commited on
Commit
a2fa1e1
Β·
verified Β·
1 Parent(s): 6d90266

deploy: update Space from deploy_preflight --push

Browse files
.codeboarding/logs/wrapper-server.log CHANGED
@@ -342,3 +342,9 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
342
  [stderr] 2026-06-14 07:40:33 INFO [watchfiles.main:308] 1 change detected
343
  [stderr] 2026-06-14 07:40:33 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
344
  [stderr] 2026-06-14 07:40:33 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
 
 
 
 
 
 
 
342
  [stderr] 2026-06-14 07:40:33 INFO [watchfiles.main:308] 1 change detected
343
  [stderr] 2026-06-14 07:40:33 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
344
  [stderr] 2026-06-14 07:40:33 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
345
+ [stderr] 2026-06-14 08:17:29 INFO [watchfiles.main:308] 3 changes detected
346
+ [stderr] 2026-06-14 08:17:29 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
347
+ [stderr] 2026-06-14 08:17:29 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
Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: setup setup-zerogpu assets run test demo bench trace preflight deploy-check deploy record record-check record-auto
2
 
3
  # Local dev uses uv (fast, locked). The HF Space still installs via pip+requirements.txt.
4
  # Entrypoints: app.py + test_core.py at root; helper scripts live in scripts/ and run
@@ -38,6 +38,9 @@ bench: ## measure model latency on this hardware (needs Ollama)
38
  trace: ## export the lesson ledger as a HF Datasets-ready trace
39
  uv run python -m scripts.export_trace
40
 
 
 
 
41
  record-check: ## recording preflight (cap-cli + Space + playwright gates)
42
  uv run python -m scripts.record --preflight-only
43
 
 
1
+ .PHONY: setup setup-zerogpu assets run test demo bench trace deliberation preflight deploy-check deploy record record-check record-auto
2
 
3
  # Local dev uses uv (fast, locked). The HF Space still installs via pip+requirements.txt.
4
  # Entrypoints: app.py + test_core.py at root; helper scripts live in scripts/ and run
 
38
  trace: ## export the lesson ledger as a HF Datasets-ready trace
39
  uv run python -m scripts.export_trace
40
 
41
+ deliberation: ## export the multi-persona deliberation as a HF Datasets-ready trace
42
+ uv run python -m scripts.export_deliberation
43
+
44
  record-check: ## recording preflight (cap-cli + Space + playwright gates)
45
  uv run python -m scripts.record --preflight-only
46
 
README.md CHANGED
@@ -64,7 +64,7 @@ The model **never grades its own outcome**: you click *Printed clean / Sagged /
64
  run the **simulated world** (a deterministic stand-in for the printer + sensors). Either way the
65
  outcome comes from outside the model. The honest signal is what makes the compounding real.
66
 
67
- **This is more than a lookup.** Two knowledge sources feed every recommendation, *RAG* (retrieved
68
  prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
69
  setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
70
  cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
@@ -167,7 +167,7 @@ own homework.
167
  MEDIA TODO (capture from the live Space at ~1600px wide; drop PNGs in assets/screenshots/):
168
  β€’ hero-build.png: Build page mid-reasoning: O'Brien's precedent read + a 3D risk flag + LAYER scrubber (the hero, top).
169
  β€’ print-loop.png: Print, the failβ†’clean quality curve + La Forge's per-iteration grades (above).
170
- β€’ second-opinion.png: Build, La Forge's πŸ” second-opinion card disputing a plan (the two-agent moment).
171
  β€’ studio.png: Studio, Benchy loaded, material + simulated environment (optional, for the writeup).
172
  β€’ review-ledger.png: Review, the ledger growing seed β†’ earned β†’ sim (optional).
173
  Demo video + social post go in the Links section below once recorded/published.
 
64
  run the **simulated world** (a deterministic stand-in for the printer + sensors). Either way the
65
  outcome comes from outside the model. The honest signal is what makes the compounding real.
66
 
67
+ **This is not just a lookup.** Two knowledge sources feed every recommendation, *RAG* (retrieved
68
  prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
69
  setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
70
  cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
 
167
  MEDIA TODO (capture from the live Space at ~1600px wide; drop PNGs in assets/screenshots/):
168
  β€’ hero-build.png: Build page mid-reasoning: O'Brien's precedent read + a 3D risk flag + LAYER scrubber (the hero, top).
169
  β€’ print-loop.png: Print, the failβ†’clean quality curve + La Forge's per-iteration grades (above).
170
+ β€’ second-opinion.png: Build, La Forge's second-opinion card disputing a plan (the two-agent moment).
171
  β€’ studio.png: Studio, Benchy loaded, material + simulated environment (optional, for the writeup).
172
  β€’ review-ledger.png: Review, the ledger growing seed β†’ earned β†’ sim (optional).
173
  Demo video + social post go in the Links section below once recorded/published.
app.py CHANGED
@@ -18,6 +18,7 @@ from __future__ import annotations
18
 
19
  import os
20
  import random
 
21
 
22
  import gradio as gr
23
 
@@ -31,6 +32,7 @@ except ImportError: # not installed locally (base env / offline); decorator no-
31
 
32
  spaces = _SpacesShim() # type: ignore
33
 
 
34
  from core import field_log
35
  from core import inspector
36
  from core import llm
@@ -124,6 +126,12 @@ _apply_model_choice("LoRA v3 (QAT E4B)")
124
  # the Off-Brand skin stays a single removable module. See ../DESIGN.md.
125
 
126
  PRINTER = "Creality Ender 3 V2"
 
 
 
 
 
 
127
  _SCROLL_TOP = "() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }"
128
 
129
 
@@ -366,14 +374,26 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
366
  confirm_vis = gr.update(visible=spine.requires_approval)
367
  approval_md = ("**HITL gate:** the Spine clamped a boundary setting β€” review, then **Confirm & Print**."
368
  if spine.requires_approval else "Within safe envelope β€” ready when you are.")
 
369
  state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
370
- "advice": rec.advice.model_dump(), "label": part.get("label")}
371
 
372
  # ── field log (Space only β€” gated on HF_TOKEN; local/offline no-ops) ──
373
  field_log.log_build(
374
  job=state["job"], env=state["env"], settings=state["settings"],
375
  advice=state["advice"], backend=rec.backend, used_fallback=rec.used_fallback,
376
  )
 
 
 
 
 
 
 
 
 
 
 
377
 
378
  return (
379
  f"{rec.backend}{fb}", # backend status
@@ -408,6 +428,10 @@ def second_opinion(state):
408
  field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
409
  "inspector_stance": verdict.stance,
410
  "inspector_headline": verdict.headline})
 
 
 
 
411
  panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
412
  if verdict.stance.lower() == "dispute":
413
  panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
@@ -428,8 +452,13 @@ def toggle_read(choice, state):
428
  return (gr.update(visible=False), gr.update(visible=True), panel, to_print, override)
429
 
430
 
431
- def ack_override():
432
  """Human overrides the Inspector's dispute β€” re-open β†’ PRINT (on the operator's call)."""
 
 
 
 
 
433
  return gr.update(interactive=True), gr.update(visible=False)
434
 
435
 
@@ -500,6 +529,20 @@ def run_print(state, iterations):
500
  "iterations": len(sess.records), "q_start": round(traj[0], 3),
501
  "q_end": round(traj[-1], 3), "first_clean": first,
502
  "inspector_stance": run_summary.stance})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  headline = (
504
  f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β€” "
505
  f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
@@ -806,7 +849,7 @@ def build() -> gr.Blocks:
806
  read_toggle.change(toggle_read, [read_toggle, state],
807
  [eng_read_group, second_op_group, second_opinion_panel,
808
  to_print_btn, override_btn])
809
- override_btn.click(ack_override, None, [to_print_btn, override_btn])
810
  to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
811
  None, None, None, js=_SCROLL_TOP)
812
  tabs.select(job_readout, [state], [p_job])
 
18
 
19
  import os
20
  import random
21
+ import uuid
22
 
23
  import gradio as gr
24
 
 
32
 
33
  spaces = _SpacesShim() # type: ignore
34
 
35
+ from core import deliberation_log
36
  from core import field_log
37
  from core import inspector
38
  from core import llm
 
126
  # the Off-Brand skin stays a single removable module. See ../DESIGN.md.
127
 
128
  PRINTER = "Creality Ender 3 V2"
129
+
130
+
131
+ def _delib_ctx(job, env) -> dict:
132
+ """Context columns shared by every deliberation-log turn for a job."""
133
+ return {"material": job.material, "geometry": job.geometry_type,
134
+ "bed_position": job.bed_position, "env_temp": env.temp, "env_humidity": env.humidity}
135
  _SCROLL_TOP = "() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }"
136
 
137
 
 
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(
383
  job=state["job"], env=state["env"], settings=state["settings"],
384
  advice=state["advice"], backend=rec.backend, used_fallback=rec.used_fallback,
385
  )
386
+ # ── deliberation log: O'Brien proposes -> Spine vetoes (same gate) ──
387
+ s = spine.settings
388
+ deliberation_log.log_turns(session_id, "preflight", [
389
+ {"agent": "O'Brien", "act": "propose",
390
+ "content": f"{rec.advice.reasoning} Proposed: nozzle {s.nozzle_temp:.0f}Β°C, bed "
391
+ f"{s.bed_temp:.0f}Β°C, fan {s.fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm."},
392
+ {"agent": "Spine", "act": "veto",
393
+ "stance": "clamped" if spine.requires_approval else "clear",
394
+ "content": ("Clamped: " + " Β· ".join(spine.vetoes)) if spine.vetoes
395
+ else "Within the safe envelope for this material β€” no clamp."},
396
+ ], _delib_ctx(job, env))
397
 
398
  return (
399
  f"{rec.backend}{fb}", # backend status
 
428
  field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
429
  "inspector_stance": verdict.stance,
430
  "inspector_headline": verdict.headline})
431
+ deliberation_log.log_turns(state.get("session_id"), "preflight", [
432
+ {"agent": "La Forge", "act": "second_opinion", "stance": verdict.stance,
433
+ "content": f"{verdict.headline} β€” {verdict.detail}"},
434
+ ], _delib_ctx(job, env))
435
  panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
436
  if verdict.stance.lower() == "dispute":
437
  panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
 
452
  return (gr.update(visible=False), gr.update(visible=True), panel, to_print, override)
453
 
454
 
455
+ def ack_override(state):
456
  """Human overrides the Inspector's dispute β€” re-open β†’ PRINT (on the operator's call)."""
457
+ if state and state.get("session_id"):
458
+ deliberation_log.log_turns(state["session_id"], "preflight", [
459
+ {"agent": "Operator", "act": "override", "stance": "override",
460
+ "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call."},
461
+ ], _delib_ctx(Job(**state["job"]), Environment(**state["env"])))
462
  return gr.update(interactive=True), gr.update(visible=False)
463
 
464
 
 
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}."})
539
+ loop_turns.append({"agent": "La Forge", "act": "grade", "stance": g.stance,
540
+ "content": f"{g.headline} β€” {g.detail}"})
541
+ deliberation_log.log_turns(state.get("session_id"), "print-loop", loop_turns, ctx)
542
+ deliberation_log.log_turns(state.get("session_id"), "review", [
543
+ {"agent": "La Forge", "act": "verdict", "stance": run_summary.stance,
544
+ "content": f"{run_summary.headline} β€” {run_summary.detail}"},
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}); "
 
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])
core/deliberation_log.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deliberation log β€” live, turn-by-turn capture of the personas' argument.
2
+
3
+ Companion to field_log.py. Where the field log records the *interaction config*,
4
+ this records *how the agent reasoned*: each run appends the turns of the
5
+ multi-persona deliberation (O'Brien proposes -> Spine vetoes -> La Forge second
6
+ opinion / dispute -> operator override -> world simulates -> La Forge grades ->
7
+ run verdict) and CommitScheduler pushes them to an open HF Dataset.
8
+
9
+ Same guarantees as field_log:
10
+ - Gated on HF_TOKEN β€” nothing is written or pushed if the secret is absent.
11
+ - Best-effort + exception-safe β€” logging never breaks a run.
12
+ - Config + agent reasoning only; no PII, no uploaded mesh files.
13
+
14
+ Schema mirrors scripts/export_deliberation.py (one row per turn) so the live
15
+ dataset and the static export share the same shape:
16
+ session_id, track, turn, agent, role, act, stance, content,
17
+ material, geometry, bed_position, env_temp, env_humidity, ts
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import threading
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ DELIB_LOG_DIR = Path(__file__).resolve().parent.parent / "deliberation_logs"
30
+ DELIB_LOG_FILE = DELIB_LOG_DIR / "deliberations.jsonl"
31
+ DELIB_LOG_REPO = "kylebrodeur/chief-engineer-deliberation"
32
+ FLUSH_MINUTES = 5
33
+
34
+ ROLE = {
35
+ "O'Brien": "Chief Engineer",
36
+ "La Forge": "QA Inspector",
37
+ "Spine": "Safety Spine",
38
+ "World": "Outcome Simulator",
39
+ "Operator": "Operator",
40
+ }
41
+
42
+ _CANON = (
43
+ "session_id", "track", "turn", "agent", "role", "act", "stance", "content",
44
+ "material", "geometry", "bed_position", "env_temp", "env_humidity", "ts",
45
+ )
46
+
47
+ _scheduler: Any = None
48
+ _lock = threading.Lock()
49
+ _turns: dict[str, int] = {} # session_id -> last turn number (single process on the Space)
50
+
51
+
52
+ def _get_scheduler():
53
+ """Lazy-init the CommitScheduler. Returns None if HF_TOKEN is missing."""
54
+ global _scheduler
55
+ token = os.environ.get("HF_TOKEN", "").strip()
56
+ if not token:
57
+ return None
58
+ if _scheduler is None:
59
+ with _lock:
60
+ if _scheduler is None:
61
+ try:
62
+ from huggingface_hub import CommitScheduler
63
+ except ImportError:
64
+ return None
65
+ DELIB_LOG_DIR.mkdir(parents=True, exist_ok=True)
66
+ if not DELIB_LOG_FILE.exists():
67
+ DELIB_LOG_FILE.write_text("", encoding="utf-8")
68
+ _scheduler = CommitScheduler(
69
+ repo_id=DELIB_LOG_REPO,
70
+ repo_type="dataset",
71
+ folder_path=str(DELIB_LOG_DIR),
72
+ every=FLUSH_MINUTES,
73
+ token=token,
74
+ allow_patterns=["*.jsonl"],
75
+ )
76
+ return _scheduler
77
+
78
+
79
+ def is_active() -> bool:
80
+ """True if deliberation logging is live (HF_TOKEN present + scheduler ready)."""
81
+ return _get_scheduler() is not None
82
+
83
+
84
+ def _next_turn(session_id: str) -> int:
85
+ with _lock:
86
+ n = _turns.get(session_id, 0) + 1
87
+ _turns[session_id] = n
88
+ return n
89
+
90
+
91
+ def log_turns(session_id: str, track: str, turns: list[dict], ctx: dict) -> bool:
92
+ """Append a batch of deliberation turns for one phase of one run.
93
+
94
+ `turns` is a list of {agent, act, content, stance?} dicts; `ctx` carries
95
+ material/geometry/bed_position/env_temp/env_humidity. Gated + exception-safe:
96
+ if HF_TOKEN is unset or anything fails, this is a silent no-op."""
97
+ try:
98
+ sched = _get_scheduler()
99
+ if sched is None or not session_id or not turns:
100
+ return False
101
+ lines: list[str] = []
102
+ for tn in turns:
103
+ agent = tn.get("agent", "")
104
+ row = {k: None for k in _CANON}
105
+ row.update({
106
+ "session_id": session_id, "track": track, "turn": _next_turn(session_id),
107
+ "agent": agent, "role": ROLE.get(agent, agent),
108
+ "act": tn.get("act"), "stance": tn.get("stance", ""),
109
+ "content": (tn.get("content") or "").strip(),
110
+ "material": ctx.get("material"), "geometry": ctx.get("geometry"),
111
+ "bed_position": ctx.get("bed_position"),
112
+ "env_temp": ctx.get("env_temp"), "env_humidity": ctx.get("env_humidity"),
113
+ "ts": datetime.now(timezone.utc).isoformat(),
114
+ })
115
+ lines.append(json.dumps(row, ensure_ascii=False))
116
+ with _lock:
117
+ with DELIB_LOG_FILE.open("a", encoding="utf-8") as f:
118
+ f.write("\n".join(lines) + "\n")
119
+ try:
120
+ sched.trigger()
121
+ except Exception:
122
+ pass
123
+ return True
124
+ except Exception:
125
+ return False # logging is best-effort β€” never break a run
core/theme.py CHANGED
@@ -199,6 +199,61 @@ textarea:focus, input:focus {{ border-color:var(--ao-orange) !important; box-sha
199
  .ce-icon-printer::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' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></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' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); }}
200
  .ce-icon-info::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' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></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' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></svg>"); }}
201
  .ce-icon-sliders::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' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></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' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></svg>"); }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  /* ── IMPLICIT GROUPING (borderless, proximity-based) ── */
204
  /* Model switcher row: sits BELOW the LCARS command bar (right-aligned).
@@ -455,6 +510,71 @@ CSS += """
455
  width:8px !important; height:300px !important; }
456
  .ce-vslider { display:flex; justify-content:center; }
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  /* segmented toggle (Engineer's Read | Second Opinion) β€” reuses pill radios, joined */
459
  .ce-seg fieldset, .ce-seg .wrap { display:flex !important; gap:0 !important; }
460
  .ce-seg label { border-radius:0 !important; border:1px solid var(--ao-outline) !important;
 
199
  .ce-icon-printer::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' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></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' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); }}
200
  .ce-icon-info::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' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></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' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></svg>"); }}
201
  .ce-icon-sliders::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' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></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' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></svg>"); }}
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
233
+ prepended via ::before. The icons are inline-svg via mask-image so they inherit
234
+ the tab's text color (orange-on-dark or dark-on-orange when selected). */
235
+ .tab-nav button::before, button[role="tab"]::before {{
236
+ content:""; display:inline-block; width:13px; height:13px; margin-right:8px;
237
+ vertical-align:-2px; background:currentColor;
238
+ -webkit-mask-size:contain; mask-size:contain;
239
+ -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
240
+ -webkit-mask-position:center; mask-position:center; }}
241
+ /* LOAD β€” inbox/upload tray icon */
242
+ .tab-nav button:nth-of-type(1)::before, button[role="tab"]:nth-of-type(1)::before {{
243
+ -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='M22 12h-6l-2 3h-4l-2-3H2 M5.5 6h13l3 6v6a2 2 0 01-2 2h-15a2 2 0 01-2-2v-6l3-6z'/></svg>");
244
+ 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='M22 12h-6l-2 3h-4l-2-3H2 M5.5 6h13l3 6v6a2 2 0 01-2 2h-15a2 2 0 01-2-2v-6l3-6z'/></svg>"); }}
245
+ /* SLICE β€” layered stack icon */
246
+ .tab-nav button:nth-of-type(2)::before, button[role="tab"]:nth-of-type(2)::before {{
247
+ -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 2l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5'/></svg>");
248
+ 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 2l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5'/></svg>"); }}
249
+ /* PRINT β€” printer icon */
250
+ .tab-nav button:nth-of-type(3)::before, button[role="tab"]:nth-of-type(3)::before {{
251
+ -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='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>");
252
+ 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='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); }}
253
+ /* REVIEW β€” clipboard-check icon */
254
+ .tab-nav button:nth-of-type(4)::before, button[role="tab"]:nth-of-type(4)::before {{
255
+ -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='M9 3h6v3H9z M9 5H6a2 2 0 00-2 2v13a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-3 M8 14l3 3 5-6'/></svg>");
256
+ 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='M9 3h6v3H9z M9 5H6a2 2 0 00-2 2v13a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-3 M8 14l3 3 5-6'/></svg>"); }}
257
 
258
  /* ── IMPLICIT GROUPING (borderless, proximity-based) ── */
259
  /* Model switcher row: sits BELOW the LCARS command bar (right-aligned).
 
510
  width:8px !important; height:300px !important; }
511
  .ce-vslider { display:flex; justify-content:center; }
512
 
513
+ /* ── HORIZONTAL LAYER SCRUBBER (replaces the broken vertical slider) ── */
514
+ /* Spans full width below the slicer image. Filled track left of the thumb,
515
+ tick marks every 5 layers, big thumb, current/max readout above. */
516
+ .ce-hslider { width:100% !important; margin:14px 0 8px !important;
517
+ padding:14px 16px !important;
518
+ background:linear-gradient(180deg, rgba(255,156,0,0.04), transparent);
519
+ border-top:1px solid var(--ao-outline-dim);
520
+ border-bottom:1px solid var(--ao-outline-dim); }
521
+ .ce-hslider > * { width:100% !important; }
522
+ .ce-hslider label, .ce-hslider .head { color:var(--ao-orange) !important;
523
+ letter-spacing:3px; text-transform:uppercase; font-weight:800 !important;
524
+ font-size:11px !important; margin-bottom:6px !important;
525
+ display:flex !important; align-items:center !important; gap:10px !important; }
526
+ .ce-hslider label::before { content:"LAYER"; }
527
+ .ce-hslider label > * { font-size:11px !important; }
528
+ .ce-hslider input[type=range] { width:100% !important; height:14px !important;
529
+ -webkit-appearance:none !important; appearance:none !important;
530
+ background:transparent !important; cursor:pointer; }
531
+ .ce-hslider input[type=range]::-webkit-slider-runnable-track {
532
+ height:6px; background:var(--ao-surface-hi);
533
+ border:1px solid var(--ao-outline-dim);
534
+ background-image:repeating-linear-gradient(90deg,
535
+ var(--ao-outline-dim) 0, var(--ao-outline-dim) 1px,
536
+ transparent 1px, transparent calc(100% / 8)); }
537
+ .ce-hslider input[type=range]::-moz-range-track {
538
+ height:6px; background:var(--ao-surface-hi);
539
+ border:1px solid var(--ao-outline-dim); }
540
+ .ce-hslider input[type=range]::-webkit-slider-thumb {
541
+ -webkit-appearance:none; appearance:none;
542
+ width:20px; height:20px; border-radius:0;
543
+ background:var(--ao-orange); border:2px solid var(--ao-orange);
544
+ margin-top:-7px; cursor:grab;
545
+ box-shadow:0 0 8px rgba(255,156,0,0.6); }
546
+ .ce-hslider input[type=range]::-webkit-slider-thumb:active { cursor:grabbing;
547
+ background:#fff; }
548
+ .ce-hslider input[type=range]::-moz-range-thumb {
549
+ width:20px; height:20px; border-radius:0;
550
+ background:var(--ao-orange); border:2px solid var(--ao-orange);
551
+ cursor:grab; box-shadow:0 0 8px rgba(255,156,0,0.6); }
552
+ /* hide gradio's number-field side (we only want the slider here) */
553
+ .ce-hslider .min, .ce-hslider .max,
554
+ .ce-hslider input[type=number],
555
+ .ce-hslider button[aria-label="Reset"],
556
+ .ce-hslider .source-selection { display:none !important; }
557
+ .ce-hslider .head .v { color:var(--ao-blue); font-weight:800; }
558
+ .ce-hslider .head .max { color:var(--ao-outline); font-weight:600; }
559
+
560
+ /* Virtual print preview β€” bigger, framed, motion-aware */
561
+ .ce-vp { padding:8px !important; border:1px solid var(--ao-outline-dim) !important;
562
+ background:var(--ao-void) !important; min-height:340px !important;
563
+ display:flex !important; flex-direction:column !important; gap:6px !important; }
564
+ .ce-vp svg { width:100% !important; height:auto !important; max-height:300px !important; }
565
+ .ce-vp .ce-vp-caption { color:var(--ao-outline); font-size:11px;
566
+ letter-spacing:1px; text-transform:uppercase; padding:4px 2px; }
567
+
568
+ /* Mini loader (Second Opinion in-flight β€” smaller than the full preflight loader).
569
+ Same scanning-bar look but condensed. */
570
+ .ce-mini-loader { display:flex; flex-direction:column; gap:8px;
571
+ padding:14px 16px; min-height:90px;
572
+ background:linear-gradient(180deg, rgba(255,156,0,0.04), transparent 80px);
573
+ border-left:3px solid var(--ao-orange); margin:6px 0; }
574
+ .ce-mini-loader .ce-loader-bar { height:4px; }
575
+ .ce-mini-loader .ce-loader-text { font-size:11px; letter-spacing:2px; }
576
+ .ce-mini-loader .ce-loader-stages > div { font-size:10px; }
577
+
578
  /* segmented toggle (Engineer's Read | Second Opinion) β€” reuses pill radios, joined */
579
  .ce-seg fieldset, .ce-seg .wrap { display:flex !important; gap:0 !important; }
580
  .ce-seg label { border-radius:0 !important; border:1px solid var(--ao-outline) !important;
dist/README.md CHANGED
@@ -1,14 +1,56 @@
1
  ---
2
  license: mit
3
- task_categories: [tabular-regression]
4
- tags: [3d-printing, additive-manufacturing, agent-trace, build-small-hackathon]
 
 
 
 
 
 
 
 
 
5
  ---
6
 
7
- # Chief Engineer β€” Lesson Ledger
8
 
9
- Environment-keyed 3D-printing lessons accumulated by **The Chief Engineer**, a small local
10
- Gemma agent built for the HF Build Small hackathon (Backyard AI). Each row is a durable lesson
11
- keyed to material, geometry, and ambient conditions β€” `seed` rows bootstrap the corpus,
12
- `earned` rows are written by the agent after a human-reported print outcome.
 
13
 
14
- Schema: `job_id, material, geometry_type, env_temp, env_humidity, outcome, lesson, source, timestamp`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: mit
3
+ language:
4
+ - en
5
+ tags:
6
+ - 3d-printing
7
+ - additive-manufacturing
8
+ - llm
9
+ - retrieval
10
+ - build-small-hackathon
11
+ pretty_name: "Microfactory Node: Lesson Ledger"
12
+ size_categories:
13
+ - n<1K
14
  ---
15
 
16
+ # Microfactory Node: 3D Printer (Lesson Ledger)
17
 
18
+ The compounding memory of **Microfactory Node: 3D Printer**, a small local Gemma that learns 3D
19
+ printing job by job. Each row is one durable lesson keyed to the conditions it was learned in.
20
+ The node retrieves from this ledger before every print, so job N+1 starts smarter than job N.
21
+ This is the knowledge that usually lives in one maker's head and dies with the shop. Here it
22
+ persists, and anyone can read it.
23
 
24
+ Project: [Live](https://node.microfactory.space) Β·
25
+ [Code](https://github.com/kylebrodeur/microfactory-lab).
26
+
27
+ ## Schema (one JSON object per line)
28
+
29
+ ```json
30
+ {"job_id": "seed-004", "material": "PLA", "geometry_type": "overhang",
31
+ "env_temp": 28.0, "env_humidity": 50.0, "outcome": "failed_sag",
32
+ "lesson": "PLA overhang sagged at 28C with the fan low; raise cooling, drop nozzle ~10C.",
33
+ "source": "seed", "timestamp": "2026-05-20T09:00:00Z"}
34
+ ```
35
+
36
+ | Field | Meaning |
37
+ |---|---|
38
+ | `material` | PLA, PETG, ABS, TPU |
39
+ | `geometry_type` | overhang, bridge, stringing, adhesion, vase (the failure-mode class) |
40
+ | `env_temp` / `env_humidity` | room conditions the lesson was learned in |
41
+ | `outcome` | success, failed_sag, failed_stringing |
42
+ | `lesson` | one durable, directional sentence (the transferable craft) |
43
+ | `source` | `seed` (curated start), `ingested` (from real configs/prints), `earned` (a real reported outcome), `sim` (the deterministic world) |
44
+
45
+ ## How the knowledge is sourced
46
+
47
+ Lessons are grounded, never invented. Seed lessons are hand-curated starters. Ingested lessons
48
+ come from real slicer/firmware profiles and real print history. Earned and simulated lessons come
49
+ from outcomes reported outside the model: the model proposes, a deterministic world or a human
50
+ reports what happened, and only then is a lesson written. The model never grades its own work.
51
+
52
+ ## Honest limits
53
+
54
+ Small and deliberately so. The simulated outcomes are a physics-lite stand-in for a printer, used
55
+ to run the closed loop on camera; they are labeled as such. Treat the lessons as a maker's
56
+ notebook: directionally true, condition-keyed, and meant to be checked against your own machine.
dist/chief_engineer_ledger.jsonl CHANGED
@@ -24,16 +24,3 @@
24
  {"job_id":"ingest-009","material":"TPU","geometry_type":"stringing","env_temp":22.0,"env_humidity":35.0,"outcome":"success","lesson":"TPU printed clean with 3mm retraction at 25mm/s in a dry room. TPU's flexibility means retraction must be slow and short β€” fast retraction stretches the filament instead of pulling it back.","source":"ingested","timestamp":"2026-06-12T12:25:00Z"}
25
  {"job_id":"ingest-010","material":"PETG","geometry_type":"adhesion","env_temp":22.0,"env_humidity":50.0,"outcome":"success","lesson":"PETG adhesion is strong on PEI at 80C bed β€” too strong on smooth PEI without release agent. Use textured PEI or glue stick as a release layer, not an adhesion aid, for PETG.","source":"ingested","timestamp":"2026-06-12T12:30:00Z"}
26
  {"job_id":"octo-petg-stringy-240","material":"PETG","geometry_type":"stringing","env_temp":22.0,"env_humidity":50.0,"outcome":"failed_stringing","lesson":"My own PETG print at 240C nozzle came out very stringy on a retraction-heavy part. PETG strings when the nozzle runs hot or the spool is damp - dry the spool, drop ~10C, and increase retraction.","source":"ingested","timestamp":"2024-05-20T20:57:00+00:00"}
27
- {"job_id":"sim-035004454637","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.39) with nozzle 235Β°C, fan 40%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.454661+00:00"}
28
- {"job_id":"sim-035004458175","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.47) with nozzle 232Β°C, fan 52%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.458194+00:00"}
29
- {"job_id":"sim-035004458709","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.53) with nozzle 229Β°C, fan 64%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.458722+00:00"}
30
- {"job_id":"sim-035004459140","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.54) with nozzle 226Β°C, fan 76%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.459152+00:00"}
31
- {"job_id":"sim-035004459597","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.55) with nozzle 223Β°C, fan 88%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.459609+00:00"}
32
- {"job_id":"sim-035004459987","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.56) with nozzle 220Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.459999+00:00"}
33
- {"job_id":"sim-035004461203","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.68) with nozzle 225Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.461215+00:00"}
34
- {"job_id":"sim-035004461453","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.61) with nozzle 222Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.461461+00:00"}
35
- {"job_id":"sim-035004462543","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.462561+00:00"}
36
- {"job_id":"sim-035004462854","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.462864+00:00"}
37
- {"job_id":"sim-035004463172","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.463184+00:00"}
38
- {"job_id":"sim-035004463498","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.463507+00:00"}
39
- {"job_id":"sim-035039090556","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.39) with nozzle 245Β°C, fan 40%, retraction 5.0mm.","source":"sim","timestamp":"2026-06-13T03:50:39.090586+00:00"}
 
24
  {"job_id":"ingest-009","material":"TPU","geometry_type":"stringing","env_temp":22.0,"env_humidity":35.0,"outcome":"success","lesson":"TPU printed clean with 3mm retraction at 25mm/s in a dry room. TPU's flexibility means retraction must be slow and short β€” fast retraction stretches the filament instead of pulling it back.","source":"ingested","timestamp":"2026-06-12T12:25:00Z"}
25
  {"job_id":"ingest-010","material":"PETG","geometry_type":"adhesion","env_temp":22.0,"env_humidity":50.0,"outcome":"success","lesson":"PETG adhesion is strong on PEI at 80C bed β€” too strong on smooth PEI without release agent. Use textured PEI or glue stick as a release layer, not an adhesion aid, for PETG.","source":"ingested","timestamp":"2026-06-12T12:30:00Z"}
26
  {"job_id":"octo-petg-stringy-240","material":"PETG","geometry_type":"stringing","env_temp":22.0,"env_humidity":50.0,"outcome":"failed_stringing","lesson":"My own PETG print at 240C nozzle came out very stringy on a retraction-heavy part. PETG strings when the nozzle runs hot or the spool is damp - dry the spool, drop ~10C, and increase retraction.","source":"ingested","timestamp":"2024-05-20T20:57:00+00:00"}
 
 
 
 
 
 
 
 
 
 
 
 
 
dist/deliberation/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ task_categories: [text-generation]
4
+ language: [en]
5
+ tags: [3d-printing, additive-manufacturing, agent-trace, multi-agent, deliberation, build-small-hackathon]
6
+ pretty_name: Chief Engineer β€” Deliberation Traces
7
+ ---
8
+
9
+ # Chief Engineer β€” Deliberation Traces
10
+
11
+ Turn-by-turn **multi-persona deliberation** from **The Chief Engineer**, a small local
12
+ Gemma agent built for the HF Build Small hackathon (Backyard AI). Where the
13
+ [lesson ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
14
+ records *what the agent learned*, this records *how it reasons*: the argument between
15
+ the personas on each job. It grows two ways: a reproducible static export
16
+ (`make deliberation`) and **live turns logged on every run of the Space** (gated on
17
+ `HF_TOKEN`; config + agent reasoning only, never PII or uploaded files).
18
+
19
+ Each row is one **turn**:
20
+
21
+ - **O'Brien** (Chief Engineer) β€” proposes settings + reasoning over precedent.
22
+ - **Spine** (Safety Spine) β€” deterministically vetoes/clamps unsafe values.
23
+ - **La Forge** (QA Inspector) β€” a separate, skeptical voice: second opinion before the
24
+ print (`concur` / `caution` / `dispute`), a grade on each run, and a run verdict.
25
+ - **Operator** β€” the human, who can override a `dispute` and proceed.
26
+ - **World** (Outcome Simulator) β€” the deterministic physics-lite world that reports the
27
+ actual print outcome (the agent never grades its own work).
28
+
29
+ The integrity rule made literal: the proposer never marks its own homework.
30
+
31
+ ## Schema
32
+
33
+ `session_id, track, turn, agent, role, act, stance, content, material, geometry,
34
+ bed_position, env_temp, env_humidity, ts`
35
+
36
+ `track` is the phase β€” `preflight` (propose β†’ veto β†’ second opinion β†’ override),
37
+ `print-loop` (simulate β†’ grade, per iteration), `review` (run verdict).
dist/deliberation/deliberations.jsonl ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"}
30
+ {"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 7, "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:30+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 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"}
learn/finetune/MODEL_CARD.md CHANGED
@@ -13,17 +13,14 @@ tags:
13
 
14
  # Microfactory Node: 3D Printer (LoRA v2)
15
 
16
- A LoRA adapter that distills the judgment of **Chief Engineer O'Brien** into
17
- the weights of Gemma 4 E4B (8B). The live node uses retrieval over a lesson
18
- ledger; this adapter is the "bake it into the weights" frontier.
19
 
20
  ## What it does
21
 
22
- Given a 3D printing job (material, geometry, room temperature/humidity),
23
- outputs structured **Advice JSON** with:
24
  - **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
25
  - **Risk regions**: where on the part, what risk, why, anchor hint
26
- - **Reasoning**: evaluation of what transfers from prior knowledge
27
 
28
  ## Training
29
 
@@ -40,16 +37,13 @@ outputs structured **Advice JSON** with:
40
  | GPU | NVIDIA A10G (24GB) |
41
  | Framework | TRL SFTTrainer + transformers 5.x |
42
 
 
 
43
  ## Dataset
44
 
45
- Generated by running the base model (`google/gemma-4-E4B-it`) over a grid of
46
- 4 materials Γ— 5 geometries Γ— 3 temperatures Γ— 3 humidities (train) and
47
- 2 temperatures Γ— 2 humidities (eval). Each example is a chat-format pair:
48
- system prompt describing the job β†’ structured Advice JSON response.
49
 
50
- Targets are **non-deterministic** (temperature=0.7, top_p=0.95) to prevent
51
- template memorization β€” the v1 adapter (Gemma 3, deterministic targets, r=16,
52
- 3 epochs) parroted identical settings for every input.
53
 
54
  ## Usage
55
 
@@ -74,9 +68,7 @@ print(tok.decode(out[0], skip_special_tokens=True))
74
 
75
  ## Safety
76
 
77
- This adapter does **judgment, not safety**. A deterministic Spine validates
78
- all settings against material bounds before any printer sees them. The LoRA
79
- proposes; the Spine vetoes.
80
 
81
  ## Iteration history
82
 
@@ -85,9 +77,23 @@ proposes; the Spine vetoes.
85
  | v1 | gemma-3-1b-it | r=16 | 3 | deterministic | ❌ Parroted template |
86
  | **v2** | **gemma-4-E4B-it** | **r=4** | **1** | **live-generated** | **βœ… Well-Tuned (100% JSON-valid, 100% Spine-safe, real judgment)** |
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  ## Try it via GGUF (Ollama / llama.cpp)
89
 
90
- A quantized GGUF of this adapter (merged into the base model) is published as
91
  [`kylebrodeur/microfactory-node-gguf` Β· `microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf)
92
  (5.1 GB, q4_k_m) and on the public Ollama registry:
93
 
 
13
 
14
  # Microfactory Node: 3D Printer (LoRA v2)
15
 
16
+ I trained this LoRA to bake Chief Engineer O'Brien's judgment into Gemma 4 E4B. The live node still reads from the lesson ledger; this adapter is what happens when I try to put that ledger into the weights instead.
 
 
17
 
18
  ## What it does
19
 
20
+ Give it a print job β€” material, geometry, room temperature and humidity β€” and it returns structured **Advice JSON**:
 
21
  - **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
22
  - **Risk regions**: where on the part, what risk, why, anchor hint
23
+ - **Reasoning**: what transfers from prior knowledge and why
24
 
25
  ## Training
26
 
 
37
  | GPU | NVIDIA A10G (24GB) |
38
  | Framework | TRL SFTTrainer + transformers 5.x |
39
 
40
+ I kept rank low and epochs at one on purpose. v1 used r=16 for three epochs on deterministic targets and parroted the same settings for every input. This run sacrifices raw capacity for actual attention to the job.
41
+
42
  ## Dataset
43
 
44
+ I generated the training set by driving the base model across a grid of 4 materials Γ— 5 geometries Γ— 3 temperatures Γ— 3 humidities (train), with 2 temperatures Γ— 2 humidities held out for eval. Each example is a chat-format pair: system prompt describing the job β†’ structured Advice JSON response.
 
 
 
45
 
46
+ I kept targets noisy β€” temperature=0.7, top_p=0.95 β€” so the model cannot memorize a single template. v1 proved that deterministic targets and a high rank just produce a parrot. Noise forces judgment.
 
 
47
 
48
  ## Usage
49
 
 
68
 
69
  ## Safety
70
 
71
+ This adapter proposes settings. It does not validate them. A deterministic Spine clamps every proposed value against hard material bounds before any printer sees them. The LoRA gives the opinion; the Spine has the veto.
 
 
72
 
73
  ## Iteration history
74
 
 
77
  | v1 | gemma-3-1b-it | r=16 | 3 | deterministic | ❌ Parroted template |
78
  | **v2** | **gemma-4-E4B-it** | **r=4** | **1** | **live-generated** | **βœ… Well-Tuned (100% JSON-valid, 100% Spine-safe, real judgment)** |
79
 
80
+ v1 taught me what not to do.
81
+
82
+ ## Limitations
83
+
84
+ This adapter is narrow by design, and it will fail loudly outside that narrow band.
85
+
86
+ - **Materials and geometries outside the training grid** β€” The grid covered four materials and five geometries. Hand it an exotic filament or an unusual geometry and it will guess confidently. That guess is extrapolation, not recall.
87
+ - **Humid PETG stringing** β€” Small Gemmas can return perfectly valid JSON with bad physics. During early driving I saw a lesson recommend slightly higher nozzle temperature to fight humid-PETG stringing, when the correct move is lower. Schema validation does not catch that. The human reads the plan before it runs.
88
+ - **Multi-tool or multi-material prints** β€” These were not in the training grid. Expect invented tool-change behavior.
89
+ - **ABS without an enclosure** β€” The model may propose settings that ignore chamber drafts. The Spine clamps individual values, but it does not model enclosure physics.
90
+ - **Mechanically risky combinations** β€” Very small layer heights paired with aggressive retraction can pass JSON schema and still fail on the bed. That is why La Forge inspects and the human decides.
91
+ - **No live sensor feedback** β€” It predicts from precedent and stops. It does not see actual bed adhesion, layer curling, or nozzle state. The printer and the human close the loop.
92
+ - **Single-epoch, low-rank LoRA** β€” It has not deeply rewritten the base model. Ask it something far from 3D printing and it answers like base Gemma. That is intentional.
93
+
94
  ## Try it via GGUF (Ollama / llama.cpp)
95
 
96
+ A quantized GGUF of this adapter, merged into the base model, is published as
97
  [`kylebrodeur/microfactory-node-gguf` Β· `microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf)
98
  (5.1 GB, q4_k_m) and on the public Ollama registry:
99
 
learn/finetune/MODEL_CARD_QAT.md CHANGED
@@ -14,20 +14,14 @@ tags:
14
 
15
  # Microfactory Node: 3D Printer (LoRA v3 QAT)
16
 
17
- A LoRA adapter that distills the judgment of **Chief Engineer O'Brien** into
18
- the weights of the QAT-trained `gemma-4-E4B-it-qat-q4_0-unquantized` model.
19
-
20
- This v3 iteration runs parallel to the standard v2 iteration, exploring whether
21
- fine-tuning directly on a Quantization-Aware-Trained (QAT) base yields higher
22
- quality after GGUF conversion and merging.
23
 
24
  ## What it does
25
 
26
- Given a 3D printing job (material, geometry, room temperature/humidity),
27
- outputs structured **Advice JSON** with:
28
  - **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
29
  - **Risk regions**: where on the part, what risk, why, anchor hint
30
- - **Reasoning**: evaluation of what transfers from prior knowledge
31
 
32
  ## Training
33
 
@@ -44,15 +38,13 @@ outputs structured **Advice JSON** with:
44
  | GPU | NVIDIA A10G (24GB) |
45
  | Framework | TRL SFTTrainer + transformers 5.x |
46
 
 
 
47
  ## Dataset
48
 
49
- Generated by running the base model (`google/gemma-4-E4B-it`) over a grid of
50
- 4 materials Γ— 5 geometries Γ— 3 temperatures Γ— 3 humidities (train) and
51
- 2 temperatures Γ— 2 humidities (eval). Each example is a chat-format pair:
52
- system prompt describing the job β†’ structured Advice JSON response.
53
 
54
- Targets are **non-deterministic** (temperature=0.7, top_p=0.95) to prevent
55
- template memorization.
56
 
57
  ## Usage
58
 
@@ -77,9 +69,7 @@ print(tok.decode(out[0], skip_special_tokens=True))
77
 
78
  ## Safety
79
 
80
- This adapter does **judgment, not safety**. A deterministic Spine validates
81
- all settings against material bounds before any printer sees them. The LoRA
82
- proposes; the Spine vetoes.
83
 
84
  ## Iteration history
85
 
@@ -89,9 +79,24 @@ proposes; the Spine vetoes.
89
  | v2 | gemma-4-E4B-it | r=4 | 1 | live-generated | βœ… Well-Tuned |
90
  | **v3** | **gemma-4-E4B-it-qat-q4_0-unquantized** | **r=4** | **1** | **live-generated** | **βœ… Well-Tuned (QAT-trained β€” better fidelity after q4_0 quant)** |
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  ## Try it via GGUF (Ollama / llama.cpp)
93
 
94
- Two quantized GGUFs of this adapter (merged into the QAT base) are published.
95
  Both live in [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf)
96
  and on the public Ollama registry:
97
 
@@ -117,4 +122,4 @@ for the merge β†’ quantize β†’ upload pipeline. The non-QAT sibling lives at
117
 
118
  ## License
119
 
120
- This adapter inherits the [Gemma license](https://ai.google.dev/gemma/terms) from its base model.
 
14
 
15
  # Microfactory Node: 3D Printer (LoRA v3 QAT)
16
 
17
+ I trained this LoRA on top of the QAT-trained `gemma-4-E4B-it-qat-q4_0-unquantized` base. It runs parallel to v2: the same O'Brien judgment, but I wanted to see if fine-tuning on a Quantization-Aware-Trained base keeps more quality after q4_0 GGUF conversion.
 
 
 
 
 
18
 
19
  ## What it does
20
 
21
+ Give it a print job β€” material, geometry, room temperature and humidity β€” and it returns structured **Advice JSON**:
 
22
  - **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
23
  - **Risk regions**: where on the part, what risk, why, anchor hint
24
+ - **Reasoning**: what transfers from prior knowledge and why
25
 
26
  ## Training
27
 
 
38
  | GPU | NVIDIA A10G (24GB) |
39
  | Framework | TRL SFTTrainer + transformers 5.x |
40
 
41
+ Same low-rank, single-epoch setup as v2. The variable is the QAT base.
42
+
43
  ## Dataset
44
 
45
+ I generated the training set by driving the base model across a grid of 4 materials Γ— 5 geometries Γ— 3 temperatures Γ— 3 humidities (train), with 2 temperatures Γ— 2 humidities held out for eval. Each example is a chat-format pair: system prompt describing the job β†’ structured Advice JSON response.
 
 
 
46
 
47
+ I kept targets noisy β€” temperature=0.7, top_p=0.95 β€” to prevent template memorization.
 
48
 
49
  ## Usage
50
 
 
69
 
70
  ## Safety
71
 
72
+ This adapter proposes settings. It does not validate them. A deterministic Spine clamps every proposed value against hard material bounds before any printer sees them. The LoRA gives the opinion; the Spine has the veto.
 
 
73
 
74
  ## Iteration history
75
 
 
79
  | v2 | gemma-4-E4B-it | r=4 | 1 | live-generated | βœ… Well-Tuned |
80
  | **v3** | **gemma-4-E4B-it-qat-q4_0-unquantized** | **r=4** | **1** | **live-generated** | **βœ… Well-Tuned (QAT-trained β€” better fidelity after q4_0 quant)** |
81
 
82
+ v1 taught me what not to do. v3 tests whether QAT pre-training helps the quantized artifact.
83
+
84
+ ## Limitations
85
+
86
+ This adapter is narrow by design, and it will fail loudly outside that narrow band.
87
+
88
+ - **Materials and geometries outside the training grid** β€” The grid covered four materials and five geometries. Hand it an exotic filament or an unusual geometry and it will guess confidently. That guess is extrapolation, not recall.
89
+ - **Humid PETG stringing** β€” Small Gemmas can return perfectly valid JSON with bad physics. During early driving I saw a lesson recommend slightly higher nozzle temperature to fight humid-PETG stringing, when the correct move is lower. Schema validation does not catch that. The human reads the plan before it runs.
90
+ - **Multi-tool or multi-material prints** β€” These were not in the training grid. Expect invented tool-change behavior.
91
+ - **ABS without an enclosure** β€” The model may propose settings that ignore chamber drafts. The Spine clamps individual values, but it does not model enclosure physics.
92
+ - **Mechanically risky combinations** β€” Very small layer heights paired with aggressive retraction can pass JSON schema and still fail on the bed. That is why La Forge inspects and the human decides.
93
+ - **No live sensor feedback** β€” It predicts from precedent and stops. It does not see actual bed adhesion, layer curling, or nozzle state. The printer and the human close the loop.
94
+ - **QAT-specific quant mismatch** β€” The QAT base was trained for q4_0. If you pick q4_k_m you get a balanced default, but it is slightly off the quant the base prepared for. Use q4_0 for highest fidelity.
95
+ - **Single-epoch, low-rank LoRA on a specialized base** β€” It has not deeply rewritten base knowledge, and the QAT base is already a specialized artifact. Ask it something far from 3D printing and it may behave less like general Gemma than v2 does. That is the trade-off.
96
+
97
  ## Try it via GGUF (Ollama / llama.cpp)
98
 
99
+ Two quantized GGUFs of this adapter, merged into the QAT base, are published.
100
  Both live in [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf)
101
  and on the public Ollama registry:
102
 
 
122
 
123
  ## License
124
 
125
+ This adapter inherits the [Gemma license](https://ai.google.dev/gemma/terms) from its base model.
learn/finetune/OLLAMA_PUBLISHING.md CHANGED
@@ -1,8 +1,6 @@
1
  # Publishing GGUFs to ollama.com β€” runbook & decision log
2
 
3
- Date: 2026-06-14. This documents how the Chief Engineer GGUFs got listed on the
4
- public Ollama registry at `ollama.com/kylebrodeur/microfactory-node-*`, so the
5
- process is reproducible (next adapter, next quant, next mistake to recover from).
6
 
7
  ## TL;DR β€” what got pushed
8
 
@@ -13,8 +11,7 @@ process is reproducible (next adapter, next quant, next mistake to recover from)
13
  | `kylebrodeur/microfactory-node-v2` | `microfactory-node-v2.gguf` | q4_k_m | 5.3 GB | Standard E4B fine-tune |
14
  | `kylebrodeur/microfactory-node` | `microfactory-node.gguf` | q4_k_m | 5.3 GB | First fine-tune (v1, historical) |
15
 
16
- All four are public on `https://ollama.com/kylebrodeur` and pullable with one
17
- command:
18
 
19
  ```bash
20
  ollama run kylebrodeur/microfactory-node-v3-qat # recommended
@@ -23,8 +20,7 @@ ollama run kylebrodeur/microfactory-node-v2
23
  ollama run kylebrodeur/microfactory-node
24
  ```
25
 
26
- > The same files are also pullable from HF directly (no ollama.com round-trip),
27
- > see the HF-direct path at the bottom of this doc.
28
 
29
  ---
30
 
@@ -56,9 +52,7 @@ Local ollama store (`/usr/share/ollama/.ollama/models/`)
56
  ollama.com/kylebrodeur/<short-name>
57
  ```
58
 
59
- Every box on the left of `ollama.com` is automated. The two manual one-time
60
- steps (described below) are: generating an Ollama SSH keypair and registering
61
- it on your ollama.com profile.
62
 
63
  ---
64
 
@@ -66,34 +60,24 @@ it on your ollama.com profile.
66
 
67
  ### 1. Ollama account on `ollama.com`
68
 
69
- Sign up at <https://ollama.com>. The username you pick becomes the namespace
70
- prefix on every model you publish (`<username>/<model>`). I used `kylebrodeur`
71
- to match the GitHub / Hugging Face username β€” keeping the three handles in sync
72
- makes documentation and cross-linking sane.
73
 
74
  ### 2. Generate an Ollama SSH key
75
 
76
- The system Ollama daemon (typically `/usr/share/ollama/.ollama/`) does NOT own
77
- your push credentials. The `ollama push` client looks for an ED25519 keypair in
78
- `~/.ollama/` for the **invoking user**. Generate it once:
79
 
80
  ```bash
81
  ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N "" -C "<your-handle>-ollama" -q
82
  cat ~/.ollama/id_ed25519.pub
83
  ```
84
 
85
- > Ollama itself will lazily create this on first push, but only inside the
86
- > daemon's home dir (e.g. `/usr/share/ollama/.ollama/`). When the daemon runs
87
- > as a different user (typical on Linux package installs), the client-side
88
- > push uses YOUR home dir β€” generate it there explicitly.
89
 
90
  ### 3. Register the public key on ollama.com
91
 
92
- Open <https://ollama.com/settings/keys>, click **Add Ollama Public Key**, paste
93
- the contents of `~/.ollama/id_ed25519.pub`. Save.
94
 
95
- That is the entire auth surface β€” no API token, no `ollama login`. Pushes
96
- authenticate via that ED25519 signature on every request.
97
 
98
  ---
99
 
@@ -103,10 +87,7 @@ Given an adapter at `kylebrodeur/microfactory-node-lora-vN` on HF Hub:
103
 
104
  ### A. Build & upload the GGUF (Modal)
105
 
106
- `learn/finetune/gguf_pipeline_modal.py` has three Modal functions
107
- (`merge` on GPU β†’ `convert_to_gguf` on CPU β†’ `upload_to_hub` on CPU) and two
108
- entrypoints (`::main` for the full pipeline, `::upload_only` for re-uploading
109
- something already on the volume).
110
 
111
  ```bash
112
  PYFILE=/home/kylebrodeur/projects/microfactory-lab/chief-engineer/learn/finetune/gguf_pipeline_modal.py
@@ -134,9 +115,7 @@ modal run "${PYFILE}::upload_only" \
134
 
135
  ### B. Pull from HF into local Ollama
136
 
137
- Ollama's native HF integration is the fastest pull path (10 MB/s+ in my run,
138
- versus ~30 MB/min via `hf download` Xet warm-up for the same file). Always
139
- specify the GGUF filename as a tag so Ollama picks the right one:
140
 
141
  ```bash
142
  ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
@@ -150,8 +129,7 @@ hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf 5.3 GB
150
 
151
  ### C. Rename for the ollama.com namespace
152
 
153
- Ollama push requires a `<username>/<name>[:<tag>]` ID. The `hf.co/...` ID is
154
- not pushable as-is. Use `ollama cp` to create the publishable alias:
155
 
156
  ```bash
157
  # default tag (becomes `:latest`)
@@ -170,9 +148,7 @@ ollama push kylebrodeur/microfactory-node-v3-qat
170
  ollama push kylebrodeur/microfactory-node-v3-qat:q4_0
171
  ```
172
 
173
- Push speed is gated by Ollama's CDN ingest (~5 MB/s in my run, ~15 min per
174
- 5 GB model). It chunks + dedups across all models you push, so the second
175
- push of a model that shares any blobs with the first is much faster.
176
 
177
  ### E. Verify
178
 
@@ -188,15 +164,13 @@ ollama run kylebrodeur/microfactory-node-v3-qat
188
  >>> PLA overhang at 22C, 45% humidity
189
  ```
190
 
191
- The model card on `ollama.com/kylebrodeur/<name>` populates automatically from
192
- the `Modelfile` metadata once the manifest is uploaded.
193
 
194
  ---
195
 
196
  ## Doing it for all 3 (or 4) variants in one shot
197
 
198
- `/tmp/all-pushes.sh` is the script we used β€” runs detached, logs to
199
- `/tmp/all-pushes.log`, processes v3-qat β†’ v2 β†’ v1 serially (pull β†’ cp β†’ push):
200
 
201
  ```bash
202
  #!/usr/bin/env bash
@@ -235,8 +209,7 @@ disown
235
  tail -f /tmp/all-pushes.log
236
  ```
237
 
238
- `setsid` + `</dev/null` + `disown` is what kept it alive across my own shell
239
- exits during the ~60 min total runtime.
240
 
241
  ---
242
 
@@ -244,19 +217,11 @@ exits during the ~60 min total runtime.
244
 
245
  ### G1 β€” Ollama keys live in YOUR home, not the daemon's
246
 
247
- On Linux package installs the daemon usually runs as the `ollama` user with
248
- `HOME=/usr/share/ollama`. `ollama push` (client) signs with whatever user is
249
- invoking it β€” so `~/.ollama/id_ed25519` must exist for *you*. Generating it
250
- with `ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N ""` is faster than
251
- poking at the daemon to lazy-create it.
252
 
253
  ### G2 β€” `hf download` via Xet was 20–30Γ— slower than `ollama pull hf.co/...`
254
 
255
- I started with `hf download kylebrodeur/microfactory-node-gguf microfactory-node-v3-qat.gguf`
256
- expecting Xet acceleration; got ~30 MB in 60 s (looked stuck). Killed it and
257
- ran `ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf`
258
- instead β€” 10–11 MB/s immediately, ETA ~8 min. The HFβ†’Ollama path uses HTTP
259
- range requests against the LFS-backed file, skipping the Xet handshake.
260
 
261
  ### G3 β€” `modal run gguf_pipeline_modal.py` fails after a second `@app.local_entrypoint()`
262
 
@@ -270,8 +235,7 @@ Error: > modal run /.../gguf_pipeline_modal.py::my_function [..args]
270
  ...
271
  ```
272
 
273
- Always pass `::main` or `::upload_only` explicitly once that second entrypoint
274
- exists.
275
 
276
  ### G4 β€” HF tokens stored as Modal secrets can carry trailing whitespace
277
 
@@ -281,24 +245,15 @@ The first upload attempt failed with:
281
  httpx.LocalProtocolError: Illegal header value b'Bearer hf_xxx '
282
  ```
283
 
284
- …because the value pasted into Modal's secret editor had a leading space and a
285
- trailing newline. The upload helper now does `token.strip()` before passing it
286
- to `HfApi`. Worth doing in any code reading Modal secrets generally.
287
 
288
  ### G5 β€” `nohup &` + `&&`-chained `modal run` lose cwd
289
 
290
- Several backgrounded `modal run` invocations failed because Modal resolved a
291
- stale `cwd` and tried to load the pipeline file from the wrong path. Two
292
- mitigations: always pass an **absolute** path to the `.py` file, and chain via
293
- `setsid` rather than the shell's job control.
294
 
295
  ### G6 β€” Overlapping filenames between Modal volume and HF repo
296
 
297
- The Modal pipeline derives both the volume path (`/out/gguf/<name>.gguf`) and
298
- the HF path (`<repo>/<name>.gguf`) from the same `--name`. Generating the q4_0
299
- variant of a model that already has a q4_k_m on HF would silently overwrite
300
- unless you split the names. Added `--as-name <hf-filename>` to `upload_only` so
301
- the HF target can differ from the volume source:
302
 
303
  ```bash
304
  modal run ...::upload_only --name microfactory-node-v3-qat \
@@ -306,24 +261,17 @@ modal run ...::upload_only --name microfactory-node-v3-qat \
306
  --repo kylebrodeur/microfactory-node-gguf
307
  ```
308
 
309
- For full pipeline runs, append `-q4_0` to `--name` directly so both layers stay
310
- in sync (the volume keeps a copy too).
311
 
312
  ### G7 β€” `ollama push` needs `<user>/<name>` exactly
313
 
314
- You cannot push a model whose ID starts with `hf.co/...` or any other
315
- non-username prefix. `ollama cp` is the only way to rename, and the new name
316
- must match `<username>/<model>` where `<username>` equals your registered
317
- ollama.com handle.
318
 
319
  ---
320
 
321
  ## HF-direct path (alternative to ollama.com)
322
 
323
- For users who just want to run the model locally without pulling from
324
- ollama.com, the same GGUFs live on HF Hub and Ollama supports `hf.co/...` URIs
325
- natively. The HF repo also holds `template`, `system`, and `params` files so
326
- the chat template, persona, and sampling apply automatically:
327
 
328
  ```bash
329
  ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
@@ -332,17 +280,13 @@ ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v2.gguf
332
  ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
333
  ```
334
 
335
- The `ollama.com/kylebrodeur/...` tags exist for discoverability via the public
336
- registry; the HF-direct URIs exist as the canonical, single-source-of-truth
337
- distribution. Both paths point at the same blobs.
338
 
339
  ---
340
 
341
  ## Cleanup
342
 
343
- The pulled `hf.co/...` tags can be removed once they've been cp'd into the
344
- `kylebrodeur/...` namespace β€” both share the same underlying blob in the
345
- content-addressed Ollama store, so deleting the alias is free:
346
 
347
  ```bash
348
  ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
@@ -351,5 +295,4 @@ ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v2.gguf
351
  ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
352
  ```
353
 
354
- The `kylebrodeur/...` tags keep working because the manifest still references
355
- the same blob digests.
 
1
  # Publishing GGUFs to ollama.com β€” runbook & decision log
2
 
3
+ Date: 2026-06-14. This documents how the Chief Engineer GGUFs got listed on the public Ollama registry at `ollama.com/kylebrodeur/microfactory-node-*`. I wrote it so the next adapter, quant, or mistake is reproducible.
 
 
4
 
5
  ## TL;DR β€” what got pushed
6
 
 
11
  | `kylebrodeur/microfactory-node-v2` | `microfactory-node-v2.gguf` | q4_k_m | 5.3 GB | Standard E4B fine-tune |
12
  | `kylebrodeur/microfactory-node` | `microfactory-node.gguf` | q4_k_m | 5.3 GB | First fine-tune (v1, historical) |
13
 
14
+ All four are public on `https://ollama.com/kylebrodeur` and pullable with one command:
 
15
 
16
  ```bash
17
  ollama run kylebrodeur/microfactory-node-v3-qat # recommended
 
20
  ollama run kylebrodeur/microfactory-node
21
  ```
22
 
23
+ The same files are also pullable from HF directly (no ollama.com round-trip). See the HF-direct path at the bottom of this doc.
 
24
 
25
  ---
26
 
 
52
  ollama.com/kylebrodeur/<short-name>
53
  ```
54
 
55
+ Every box left of `ollama.com` is automated. The two manual one-time steps are generating an Ollama SSH keypair and registering it on your ollama.com profile.
 
 
56
 
57
  ---
58
 
 
60
 
61
  ### 1. Ollama account on `ollama.com`
62
 
63
+ Sign up at <https://ollama.com>. The username you pick becomes the namespace prefix on every model you publish (`<username>/<model>`). I used `kylebrodeur` to match my GitHub and Hugging Face handles. Keeping the three in sync makes documentation and cross-linking sane.
 
 
 
64
 
65
  ### 2. Generate an Ollama SSH key
66
 
67
+ The system Ollama daemon (typically `/usr/share/ollama/.ollama/`) does NOT own your push credentials. The `ollama push` client looks for an ED25519 keypair in `~/.ollama/` for the invoking user. Generate it once:
 
 
68
 
69
  ```bash
70
  ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N "" -C "<your-handle>-ollama" -q
71
  cat ~/.ollama/id_ed25519.pub
72
  ```
73
 
74
+ Ollama itself will lazily create this on first push, but only inside the daemon's home dir (e.g. `/usr/share/ollama/.ollama/`). When the daemon runs as a different user (typical on Linux package installs), the client-side push uses YOUR home dir. Generate it there explicitly.
 
 
 
75
 
76
  ### 3. Register the public key on ollama.com
77
 
78
+ Open <https://ollama.com/settings/keys>, click Add Ollama Public Key, paste the contents of `~/.ollama/id_ed25519.pub`. Save.
 
79
 
80
+ That is the entire auth surface β€” no API token, no `ollama login`. Pushes authenticate via that ED25519 signature on every request.
 
81
 
82
  ---
83
 
 
87
 
88
  ### A. Build & upload the GGUF (Modal)
89
 
90
+ `learn/finetune/gguf_pipeline_modal.py` has three Modal functions (`merge` on GPU β†’ `convert_to_gguf` on CPU β†’ `upload_to_hub` on CPU) and two entrypoints (`::main` for the full pipeline, `::upload_only` for re-uploading something already on the volume).
 
 
 
91
 
92
  ```bash
93
  PYFILE=/home/kylebrodeur/projects/microfactory-lab/chief-engineer/learn/finetune/gguf_pipeline_modal.py
 
115
 
116
  ### B. Pull from HF into local Ollama
117
 
118
+ Ollama's native HF integration is the fastest pull path. I saw 10 MB/s+ versus ~30 MB/min via `hf download` Xet warm-up for the same file. Always specify the GGUF filename as a tag so Ollama picks the right one:
 
 
119
 
120
  ```bash
121
  ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
 
129
 
130
  ### C. Rename for the ollama.com namespace
131
 
132
+ Ollama push requires a `<username>/<name>[:<tag>]` ID. The `hf.co/...` ID is not pushable as-is. Use `ollama cp` to create the publishable alias:
 
133
 
134
  ```bash
135
  # default tag (becomes `:latest`)
 
148
  ollama push kylebrodeur/microfactory-node-v3-qat:q4_0
149
  ```
150
 
151
+ Push speed is gated by Ollama's CDN ingest (~5 MB/s in my run, ~15 min per 5 GB model). It chunks + dedups across all models you push, so the second push of a model that shares any blobs with the first is much faster.
 
 
152
 
153
  ### E. Verify
154
 
 
164
  >>> PLA overhang at 22C, 45% humidity
165
  ```
166
 
167
+ The model card on `ollama.com/kylebrodeur/<name>` populates automatically from the `Modelfile` metadata once the manifest is uploaded.
 
168
 
169
  ---
170
 
171
  ## Doing it for all 3 (or 4) variants in one shot
172
 
173
+ `/tmp/all-pushes.sh` is the script I used. It runs detached, logs to `/tmp/all-pushes.log`, and processes v3-qat β†’ v2 β†’ v1 serially (pull β†’ cp β†’ push):
 
174
 
175
  ```bash
176
  #!/usr/bin/env bash
 
209
  tail -f /tmp/all-pushes.log
210
  ```
211
 
212
+ `setsid` + `</dev/null` + `disown` is what kept it alive across my own shell exits during the ~60 min total runtime.
 
213
 
214
  ---
215
 
 
217
 
218
  ### G1 β€” Ollama keys live in YOUR home, not the daemon's
219
 
220
+ On Linux package installs the daemon usually runs as the `ollama` user with `HOME=/usr/share/ollama`. `ollama push` (client) signs with whatever user is invoking it β€” so `~/.ollama/id_ed25519` must exist for you. Generating it with `ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N ""` is faster than poking at the daemon to lazy-create it.
 
 
 
 
221
 
222
  ### G2 β€” `hf download` via Xet was 20–30Γ— slower than `ollama pull hf.co/...`
223
 
224
+ I started with `hf download kylebrodeur/microfactory-node-gguf microfactory-node-v3-qat.gguf` expecting Xet acceleration; got ~30 MB in 60 s (looked stuck). Killed it and ran `ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf` instead β€” 10–11 MB/s immediately, ETA ~8 min. The HFβ†’Ollama path uses HTTP range requests against the LFS-backed file, skipping the Xet handshake.
 
 
 
 
225
 
226
  ### G3 β€” `modal run gguf_pipeline_modal.py` fails after a second `@app.local_entrypoint()`
227
 
 
235
  ...
236
  ```
237
 
238
+ Always pass `::main` or `::upload_only` explicitly once that second entrypoint exists.
 
239
 
240
  ### G4 β€” HF tokens stored as Modal secrets can carry trailing whitespace
241
 
 
245
  httpx.LocalProtocolError: Illegal header value b'Bearer hf_xxx '
246
  ```
247
 
248
+ …because the value pasted into Modal's secret editor had a leading space and a trailing newline. The upload helper now does `token.strip()` before passing it to `HfApi`. Worth doing in any code reading Modal secrets generally.
 
 
249
 
250
  ### G5 β€” `nohup &` + `&&`-chained `modal run` lose cwd
251
 
252
+ Several backgrounded `modal run` invocations failed because Modal resolved a stale `cwd` and tried to load the pipeline file from the wrong path. Two mitigations: always pass an absolute path to the `.py` file, and chain via `setsid` rather than the shell's job control.
 
 
 
253
 
254
  ### G6 β€” Overlapping filenames between Modal volume and HF repo
255
 
256
+ The Modal pipeline derives both the volume path (`/out/gguf/<name>.gguf`) and the HF path (`<repo>/<name>.gguf`) from the same `--name`. Generating the q4_0 variant of a model that already has a q4_k_m on HF would silently overwrite unless you split the names. I added `--as-name <hf-filename>` to `upload_only` so the HF target can differ from the volume source:
 
 
 
 
257
 
258
  ```bash
259
  modal run ...::upload_only --name microfactory-node-v3-qat \
 
261
  --repo kylebrodeur/microfactory-node-gguf
262
  ```
263
 
264
+ For full pipeline runs, append `-q4_0` to `--name` directly so both layers stay in sync. The volume keeps a copy too.
 
265
 
266
  ### G7 β€” `ollama push` needs `<user>/<name>` exactly
267
 
268
+ You cannot push a model whose ID starts with `hf.co/...` or any other non-username prefix. `ollama cp` is the only way to rename, and the new name must match `<username>/<model>` where `<username>` equals your registered ollama.com handle.
 
 
 
269
 
270
  ---
271
 
272
  ## HF-direct path (alternative to ollama.com)
273
 
274
+ For users who just want to run the model locally without pulling from ollama.com, the same GGUFs live on HF Hub and Ollama supports `hf.co/...` URIs natively. The HF repo also holds `template`, `system`, and `params` files so the chat template, persona, and sampling apply automatically:
 
 
 
275
 
276
  ```bash
277
  ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
 
280
  ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
281
  ```
282
 
283
+ The `ollama.com/kylebrodeur/...` tags exist for discoverability via the public registry. The HF-direct URIs are the canonical, single-source-of-truth distribution. Both paths point at the same blobs.
 
 
284
 
285
  ---
286
 
287
  ## Cleanup
288
 
289
+ The pulled `hf.co/...` tags can be removed once they've been cp'd into the `kylebrodeur/...` namespace. Both share the same underlying blob in the content-addressed Ollama store, so deleting the alias is free:
 
 
290
 
291
  ```bash
292
  ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
 
295
  ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
296
  ```
297
 
298
+ The `kylebrodeur/...` tags keep working because the manifest still references the same blob digests.
 
learn/finetune/README.md CHANGED
@@ -1,26 +1,15 @@
1
  # Fine-tune track: the "Well-Tuned" frontier, realized (parallel, optional)
2
 
3
- The live node decides with **retrieval + a learned policy + a deterministic Spine** (visible
4
- memory, by design). This track distills that same judgment into a **LoRA on a small Gemma** so it
5
- can live in the weights too. It runs **in parallel** (train while you record) and **never touches
6
- the live Space**: the demo stays the retrieval system. We only claim the **Well-Tuned** badge if a
7
- held-out eval earns it.
8
 
9
  ## Files
10
- - `prep_dataset.py`: builds `data/finetune/sft.{train,eval}.jsonl` (400 train / 80 held-out) by
11
- distilling the node's own Advice over a grid of (material, geometry, room). Already generated and
12
- committed. Offline targets are the deterministic advisor; start `ollama serve` first to distill
13
- the live Gemma instead (higher fidelity, same format).
14
  - `train_modal.py`: Modal LoRA SFT (A10G, ~1 hr cap), pushes the adapter to the Hub.
15
- - `eval.py`: honest base-vs-LoRA scoring on the held-out rooms (JSON-valid, Spine-safe).
16
- - `gguf_pipeline_modal.py`: end-to-end LoRA β†’ merged HF β†’ quantized GGUF β†’ (optional) HF Hub
17
- upload. Two entrypoints: `::main` (full pipeline) and `::upload_only` (re-upload existing
18
- volume file with optional `--as-name` to rewrite the HF filename, e.g. q4_0 suffix).
19
- - `SERVING.md`: production hosting playbook β€” ZeroGPU, Modal-hosted OpenAI endpoint, model
20
- switching in the Gradio app, current quant/HF/ollama table.
21
- - `OLLAMA_PUBLISHING.md`: end-to-end walkthrough for publishing GGUFs to **ollama.com**
22
- (one-time SSH-key setup, the `ollama pull β†’ ollama cp β†’ ollama push` flow, the seven
23
- gotchas hit and the fixes β€” read this before publishing the next adapter).
24
 
25
  ## Published artifacts
26
 
@@ -30,15 +19,14 @@ held-out eval earns it.
30
  - Modal inference API β€” `https://kylebrodeur--microfactory-node-inference-serve.modal.run`
31
 
32
  ## Honesty guardrails (non-negotiable)
 
33
  - The live Space stays retrieval-based. The LoRA is the named frontier, realized, not a swap.
34
- - Targets are the node's structured output, a faithful distillation, not invented data.
35
- - **Claim Well-Tuned only if** `eval.py` shows TUNED >= BASE on json-valid AND spine-safe, and the
36
- sampled advice reads as real judgment, not a memorized template. If it only parrots, report that
37
- and stay "Not Well-Tuned." A thin badge is worse than an honest no.
38
 
39
  ## Budget
40
- ~$100 Modal available plus run credits. A LoRA on ~400 short examples on one A10G is roughly a
41
- 30 to 60 min run, well under $2. No reason to exceed it; the dataset is small on purpose.
42
 
43
  ---
44
 
 
1
  # Fine-tune track: the "Well-Tuned" frontier, realized (parallel, optional)
2
 
3
+ I run the live node on retrieval plus a learned policy plus the deterministic Spine. That keeps the memory visible. This track bakes the same judgment into a small Gemma LoRA so it lives in the weights too. I train it in parallel while I record. It never touches the live Space. I only claim the Well-Tuned badge if a held-out eval earns it.
 
 
 
 
4
 
5
  ## Files
6
+
7
+ - `prep_dataset.py`: builds `data/finetune/sft.{train,eval}.jsonl` (400 train / 80 held-out) by distilling the node's own Advice over a grid of material, geometry, and room. I already generated and committed this. Offline targets come from the deterministic advisor. Start `ollama serve` first to distill the live Gemma instead; the format stays the same.
 
 
8
  - `train_modal.py`: Modal LoRA SFT (A10G, ~1 hr cap), pushes the adapter to the Hub.
9
+ - `eval.py`: honest base-vs-LoRA scoring on the held-out rooms. Checks JSON validity and Spine safety.
10
+ - `gguf_pipeline_modal.py`: end-to-end LoRA β†’ merged HF β†’ quantized GGUF β†’ optional HF Hub upload. Two entrypoints: `::main` (full pipeline) and `::upload_only` (re-upload an existing volume file; use `--as-name` to rewrite the HF filename, e.g. q4_0 suffix).
11
+ - `SERVING.md`: production hosting playbook β€” ZeroGPU, Modal-hosted OpenAI endpoint, model switching in the Gradio app, current quant/HF/Ollama table.
12
+ - `OLLAMA_PUBLISHING.md`: end-to-end walkthrough for publishing GGUFs to ollama.com. Read this before publishing the next adapter.
 
 
 
 
 
13
 
14
  ## Published artifacts
15
 
 
19
  - Modal inference API β€” `https://kylebrodeur--microfactory-node-inference-serve.modal.run`
20
 
21
  ## Honesty guardrails (non-negotiable)
22
+
23
  - The live Space stays retrieval-based. The LoRA is the named frontier, realized, not a swap.
24
+ - Targets are the node's structured output. I distill them faithfully. I do not invent data.
25
+ - I only claim Well-Tuned if `eval.py` shows TUNED >= BASE on json-valid AND spine-safe, and the sampled advice reads as real judgment, not a memorized template. If it only parrots, I report that and stay "Not Well-Tuned." A thin badge is worse than an honest no.
 
 
26
 
27
  ## Budget
28
+
29
+ I have ~$100 Modal available plus run credits. A LoRA on ~400 short examples on one A10G is roughly a 30 to 60 min run, well under $2. There is no reason to exceed that. The dataset is small on purpose.
30
 
31
  ---
32
 
learn/finetune/SERVING.md CHANGED
@@ -1,8 +1,6 @@
1
- # Serving & Deployment: Ollama, Modal, and Gradio Model Switching
2
 
3
- Research and recommendations for publishing the fine-tuned LoRA adapters
4
- to Ollama, hosting inference on Modal, and adding on-demand model switching
5
- to the Gradio app.
6
 
7
  ## Model Registry
8
 
@@ -17,9 +15,9 @@ to the Gradio app.
17
 
18
  ## 1. Ollama Publishing β€” Implemented
19
 
20
- ### Status: βœ… GGUFs LIVE on both HF Hub and ollama.com
21
 
22
- **Public Ollama registry** β€” [`ollama.com/kylebrodeur`](https://ollama.com/kylebrodeur):
23
 
24
  | `ollama run …` | Quant | Size |
25
  |----------------|-------|------|
@@ -28,7 +26,7 @@ to the Gradio app.
28
  | [`kylebrodeur/microfactory-node-v2`](https://ollama.com/kylebrodeur/microfactory-node-v2) | q4_k_m | 5.3 GB |
29
  | [`kylebrodeur/microfactory-node`](https://ollama.com/kylebrodeur/microfactory-node) | q4_k_m | 5.3 GB |
30
 
31
- **HF Hub repo** β€” [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf):
32
 
33
  | File | Source adapter | Variant |
34
  |------|----------------|---------|
@@ -37,35 +35,26 @@ to the Gradio app.
37
  | [`microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf) (5.1 GB, q4_k_m) | [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) | Standard E4B fine-tune |
38
  | [`microfactory-node.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node.gguf) (5.1 GB, q4_k_m) | `kylebrodeur/microfactory-node-lora` (v1) | First fine-tune (historical) |
39
 
40
- The HF Hub repo also carries Ollama config files (`template`, `system`, `params`) so the
41
- `hf.co/...` URI form of `ollama run` applies the right chat template, persona, and sampling
42
- automatically (see [Ollama HF docs](https://huggingface.co/docs/hub/en/ollama)).
43
-
44
  Users can pull either way:
45
  ```bash
46
  ollama run kylebrodeur/microfactory-node-v3-qat # via ollama.com
47
  ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf # via HF Hub
48
  ```
49
 
50
- Both LoRA adapter repos remain on the Hub for further fine-tuning / inspection:
51
  - [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) (35MB, Standard E4B)
52
  - [`kylebrodeur/microfactory-node-lora-v3-qat`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v3-qat) (35MB, QAT-unquantized)
53
 
54
- For the full ollama.com publishing walkthrough (one-time SSH-key setup, the `ollama pull` β†’
55
- `ollama cp` β†’ `ollama push` flow, the gotchas hit and fixed), see
56
- [`OLLAMA_PUBLISHING.md`](OLLAMA_PUBLISHING.md).
57
 
58
  ### Implemented: `gguf_pipeline_modal.py`
59
 
60
- **No local llama.cpp needed.** The full merge→GGUF→upload pipeline runs entirely on Modal:
61
  1. **GPU step** (`merge`): loads base model + LoRA adapter, merges via `merge_and_unload()`
62
- 2. **CPU step** (`convert_to_gguf`): clones llama.cpp, builds with cmake, runs
63
- `convert_hf_to_gguf.py` β†’ bf16 GGUF β†’ `llama-quantize` to target type (q4_k_m default)
64
- 3. **CPU step** (`upload_to_hub`, optional): pushes the GGUF to a HF Hub model repo using
65
- `chief-engineer-secrets` + `HF_TOKEN` Modal secrets
66
 
67
- The `--upload <owner>/<repo>` flag triggers step 3 inline. Without it, the GGUF stays on the
68
- Modal volume only.
69
 
70
  **v3-qat with auto-upload (single command):**
71
  ```bash
@@ -75,8 +64,7 @@ modal run learn/finetune/gguf_pipeline_modal.py::main \
75
  --upload kylebrodeur/microfactory-node-gguf
76
  ```
77
 
78
- **Upload an existing GGUF already on the volume** (used to push v1/v2 which were converted
79
- before the upload step existed):
80
  ```bash
81
  modal run learn/finetune/gguf_pipeline_modal.py::upload_only \
82
  --name microfactory-node-v2 \
@@ -98,13 +86,9 @@ modal run learn/finetune/gguf_pipeline_modal.py::main \
98
  ```
99
 
100
  **Gotchas (encountered during the v1/v2/v3 push):**
101
- - `modal run gguf_pipeline_modal.py` (without `::main`) fails once you add a second
102
- `@app.local_entrypoint()` β€” always pass `::main` or `::upload_only`.
103
- - Modal secret values can include trailing whitespace/newlines β€” the upload helper now strips
104
- the token (`token = token.strip()`); otherwise `httpx` raises
105
- `LocalProtocolError: Illegal header value b'Bearer hf_xxx '`.
106
- - Use absolute paths to the pipeline file when launching with `nohup &` β€” backgrounded
107
- shells lose cwd faster than you'd expect.
108
 
109
  **After download β€” Ollama import:**
110
  ```bash
@@ -125,24 +109,23 @@ ollama push kylebrodeur/microfactory-node-v2
125
  ```
126
 
127
  ### Decision: Mergeβ†’GGUF over adapter paths
128
- Chose the merge path (single GGUF file) over Path A (LoRA→GGUF adapter) and
129
- Path B (Ollama ADAPTER command) because:
130
- - Single GGUF file = no runtime adapter complexity
131
- - Ollama ADAPTER command only documented for Gemma 1/2, unverified for Gemma 4
132
- - `convert_lora_to_gguf.py` compatibility with Gemma 4 not tested
133
- - Merge→GGUF is the most battle-tested path
134
 
135
  ### Quantization
136
 
137
  The GGUF pipeline uses `--outtype q4_k_m` by default:
138
- - **Does NOT reduce parameter count**: 8B params stay 8B. Quantization reduces
139
- weight precision from 16-bit (bf16) to 4-bit per weight.
140
  - **File size**: ~5-6GB for 8B model (vs ~16GB for bf16 safetensors)
141
  - **Quality**: q4_k_m is the recommended balance of size vs quality
142
- - **v3 advantage**: QAT-trained model retains more quality after quantization
143
- because it was trained with simulated quantization during fine-tuning
144
 
145
  Other quantization options (pass `--outtype` to override):
 
146
  | Format | Size (8B) | Quality | Use Case |
147
  |--------|-----------|---------|----------|
148
  | q8_0 | ~8.5GB | Near-lossless | Max quality, local GPU |
@@ -151,8 +134,8 @@ Other quantization options (pass `--outtype` to override):
151
  | q4_0 | ~4.5GB | Lower | QAT-optimized (Google's QAT Q4_0 target) |
152
 
153
  ### Parallel GGUF runs
154
- Both tracks can run simultaneously β€” the pipeline uses separate Modal functions
155
- (merge on GPU, convert on CPU) and writes distinct output filenames via `--name`:
156
 
157
  ```bash
158
  # Terminal 1 β€” v2:
@@ -173,14 +156,11 @@ Download both: `modal volume get microfactory-node-finetune gguf/ --force`
173
 
174
  ## 2. Modal Model Hosting β€” Implemented
175
 
176
- ### Status: βœ… Deploying (ap-60wirJOd35PZl1ZIKakD9v)
177
 
178
  ### Implemented: `modal_serve.py`
179
 
180
- OpenAI-compatible `/v1/chat/completions` endpoint on Modal GPU.
181
- Loads base model + LoRA adapter once at container start, keeps warm.
182
- Auto-scales to zero after 5 min idle (`scaledown_window=300`).
183
- Handles up to 10 concurrent requests (`@modal.concurrent(max_inputs=10)`).
184
 
185
  Deploy:
186
  ```bash
@@ -200,12 +180,14 @@ FINETUNE_ADAPTER=kylebrodeur/microfactory-node-lora-v3-qat modal deploy learn/fi
200
  ```
201
 
202
  ### Modal API Deprecation Fixes Applied
203
- During deployment, two Modal SDK deprecations were hit and fixed:
 
204
  1. `container_idle_timeout` β†’ `scaledown_window` (deprecated 2025-02-24)
205
  2. `allow_concurrent_inputs` β†’ `@modal.concurrent(max_inputs=10)` decorator (deprecated 2025-04-09)
206
 
207
  ### Budget: Separate $100 serving budget
208
- Distinct from the ~$11.54 training budget already spent. Serving costs:
 
209
  - A10G active: ~$5.04/hr
210
  - With scale-to-zero: ~$0.50-2.00/day typical
211
  - Health check endpoint at `/health` for monitoring
@@ -214,17 +196,15 @@ Distinct from the ~$11.54 training budget already spent. Serving costs:
214
 
215
  ## 3. Gradio Model Switching β€” Backend Implemented, UI Deferred
216
 
217
- ### Status: βœ… Backend ready, UI placement deferred to other agent
218
 
219
  ### Implemented: `core/llm_zerogpu_lora.py`
220
 
221
- LoRA-aware ZeroGPU backend. Same API as `llm_zerogpu.py` (`chat_json`, `warm`, `backend_status`)
222
- but wraps the base model with `PeftModel.from_pretrained()` when `CHIEF_ENGINEER_LORA_REPO` is set.
223
- Import-guarded β€” safe no-op if torch/transformers absent.
224
 
225
  ### Implemented: `app.py` backend infrastructure
226
 
227
- Added to `app.py` (merged with UI agent's concurrent changes):
228
  - `MODEL_OPTIONS` list: "Base (Gemma 4 E4B)", "LoRA v2 (Standard E4B)", "LoRA v3 (QAT E4B)", "Modal API (remote)"
229
  - `MODEL_LORA_MAP` dict: maps UI labels β†’ HF Hub adapter repo IDs
230
  - `_apply_model_choice()` function: sets `CHIEF_ENGINEER_LORA_REPO` and `CHIEF_ENGINEER_BACKEND` env vars, reloads `core.llm` module
@@ -232,22 +212,21 @@ Added to `app.py` (merged with UI agent's concurrent changes):
232
  - `core.llm_zerogpu_lora` imported at Space startup alongside `core.llm_zerogpu`
233
 
234
  ### UI placement rolled back
235
- Per user request (another agent is handling the Gradio UI), the dropdown widget
236
- placement and HTML note were removed from `app.py`. The backend infrastructure
237
- remains so the UI agent can wire it in.
238
 
239
- ### 🀝 UI Agent Handoff (2026-06-14)
 
 
240
 
241
  **Already done (do NOT re-implement):**
242
- - βœ… `core/llm_zerogpu_lora.py` β€” LoRA-aware ZeroGPU backend
243
- - βœ… `app.py` β€” `_apply_model_choice()` function, `MODEL_OPTIONS` list, `MODEL_LORA_MAP` dict
244
- - βœ… `app.py` β€” `build_job()` now accepts `model_choice` parameter
245
- - βœ… `app.py` β€” `core.llm_zerogpu_lora` imported at startup
246
 
247
  **What the UI agent needs to do:**
248
  1. Add a `gr.Dropdown` with `MODEL_OPTIONS` choices in the STUDIO tab
249
  2. Wire `model_choice` into the `build_job` call in the event handler
250
- 3. Add info line: "πŸ’‘ Local users: get LoRA models from HF Hub or `ollama pull`"
251
  4. `_apply_model_choice()` handles all backend switching automatically
252
 
253
  ---
@@ -269,7 +248,7 @@ remains so the UI agent can wire it in.
269
  ## 5. Files Created/Modified
270
 
271
  | File | Action | Purpose |
272
- |------|--------|--------|
273
  | `core/llm_zerogpu.py` | ✏️ Modified | E2Bβ†’E4B fix |
274
  | `core/llm_zerogpu_lora.py` | ✨ Created | LoRA-aware ZeroGPU backend |
275
  | `app.py` | ✏️ Modified | Add model selector dropdown + wiring |
 
1
+ # Serving and deployment: Ollama, Modal, and Gradio model switching
2
 
3
+ This is what I figured out for publishing fine-tuned LoRA adapters to Ollama, hosting inference on Modal, and adding on-demand model switching to the Gradio app.
 
 
4
 
5
  ## Model Registry
6
 
 
15
 
16
  ## 1. Ollama Publishing β€” Implemented
17
 
18
+ ### Status: GGUFs live on both HF Hub and ollama.com
19
 
20
+ The public Ollama registry is at [`ollama.com/kylebrodeur`](https://ollama.com/kylebrodeur):
21
 
22
  | `ollama run …` | Quant | Size |
23
  |----------------|-------|------|
 
26
  | [`kylebrodeur/microfactory-node-v2`](https://ollama.com/kylebrodeur/microfactory-node-v2) | q4_k_m | 5.3 GB |
27
  | [`kylebrodeur/microfactory-node`](https://ollama.com/kylebrodeur/microfactory-node) | q4_k_m | 5.3 GB |
28
 
29
+ The HF Hub repo is [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf):
30
 
31
  | File | Source adapter | Variant |
32
  |------|----------------|---------|
 
35
  | [`microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf) (5.1 GB, q4_k_m) | [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) | Standard E4B fine-tune |
36
  | [`microfactory-node.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node.gguf) (5.1 GB, q4_k_m) | `kylebrodeur/microfactory-node-lora` (v1) | First fine-tune (historical) |
37
 
 
 
 
 
38
  Users can pull either way:
39
  ```bash
40
  ollama run kylebrodeur/microfactory-node-v3-qat # via ollama.com
41
  ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf # via HF Hub
42
  ```
43
 
44
+ I keep the LoRA adapter repos on the Hub for further fine-tuning or inspection:
45
  - [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) (35MB, Standard E4B)
46
  - [`kylebrodeur/microfactory-node-lora-v3-qat`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v3-qat) (35MB, QAT-unquantized)
47
 
48
+ For the full ollama.com publishing walkthrough, see [`OLLAMA_PUBLISHING.md`](OLLAMA_PUBLISHING.md).
 
 
49
 
50
  ### Implemented: `gguf_pipeline_modal.py`
51
 
52
+ You do not need a local llama.cpp build. The full merge→GGUF→upload pipeline runs entirely on Modal:
53
  1. **GPU step** (`merge`): loads base model + LoRA adapter, merges via `merge_and_unload()`
54
+ 2. **CPU step** (`convert_to_gguf`): clones llama.cpp, builds with cmake, runs `convert_hf_to_gguf.py` β†’ bf16 GGUF β†’ `llama-quantize` to target type (q4_k_m default)
55
+ 3. **CPU step** (`upload_to_hub`, optional): pushes the GGUF to a HF Hub model repo using `chief-engineer-secrets` + `HF_TOKEN` Modal secrets
 
 
56
 
57
+ The `--upload <owner>/<repo>` flag triggers step 3 inline. Without it, the GGUF stays on the Modal volume only.
 
58
 
59
  **v3-qat with auto-upload (single command):**
60
  ```bash
 
64
  --upload kylebrodeur/microfactory-node-gguf
65
  ```
66
 
67
+ **Upload an existing GGUF already on the volume** (used to push v1/v2 which were converted before the upload step existed):
 
68
  ```bash
69
  modal run learn/finetune/gguf_pipeline_modal.py::upload_only \
70
  --name microfactory-node-v2 \
 
86
  ```
87
 
88
  **Gotchas (encountered during the v1/v2/v3 push):**
89
+ - `modal run gguf_pipeline_modal.py` (without `::main`) fails once you add a second `@app.local_entrypoint()` β€” always pass `::main` or `::upload_only`.
90
+ - Modal secret values can include trailing whitespace/newlines β€” the upload helper now strips the token (`token = token.strip()`); otherwise `httpx` raises `LocalProtocolError: Illegal header value b'Bearer hf_xxx '`.
91
+ - Use absolute paths to the pipeline file when launching with `nohup &` β€” backgrounded shells lose cwd faster than you'd expect.
 
 
 
 
92
 
93
  **After download β€” Ollama import:**
94
  ```bash
 
109
  ```
110
 
111
  ### Decision: Mergeβ†’GGUF over adapter paths
112
+
113
+ I chose the merge path (single GGUF file) over Path A (LoRA→GGUF adapter) and Path B (Ollama ADAPTER command) because:
114
+ - A single GGUF file means no runtime adapter complexity.
115
+ - Ollama ADAPTER command is only documented for Gemma 1/2. I have not verified it for Gemma 4.
116
+ - I have not tested `convert_lora_to_gguf.py` compatibility with Gemma 4.
117
+ - Merge→GGUF is the most battle-tested path.
118
 
119
  ### Quantization
120
 
121
  The GGUF pipeline uses `--outtype q4_k_m` by default:
122
+ - **Does NOT reduce parameter count**: 8B params stay 8B. Quantization reduces weight precision from 16-bit (bf16) to 4-bit per weight.
 
123
  - **File size**: ~5-6GB for 8B model (vs ~16GB for bf16 safetensors)
124
  - **Quality**: q4_k_m is the recommended balance of size vs quality
125
+ - **v3 advantage**: QAT-trained model retains more quality after quantization because it was trained with simulated quantization during fine-tuning
 
126
 
127
  Other quantization options (pass `--outtype` to override):
128
+
129
  | Format | Size (8B) | Quality | Use Case |
130
  |--------|-----------|---------|----------|
131
  | q8_0 | ~8.5GB | Near-lossless | Max quality, local GPU |
 
134
  | q4_0 | ~4.5GB | Lower | QAT-optimized (Google's QAT Q4_0 target) |
135
 
136
  ### Parallel GGUF runs
137
+
138
+ Both tracks can run at the same time. The pipeline uses separate Modal functions (merge on GPU, convert on CPU) and writes distinct output filenames via `--name`:
139
 
140
  ```bash
141
  # Terminal 1 β€” v2:
 
156
 
157
  ## 2. Modal Model Hosting β€” Implemented
158
 
159
+ ### Status: Deploying (`ap-60wirJOd35PZl1ZIKakD9v`)
160
 
161
  ### Implemented: `modal_serve.py`
162
 
163
+ OpenAI-compatible `/v1/chat/completions` endpoint on Modal GPU. It loads base model + LoRA adapter once at container start and keeps warm. It auto-scales to zero after 5 min idle (`scaledown_window=300`). It handles up to 10 concurrent requests (`@modal.concurrent(max_inputs=10)`).
 
 
 
164
 
165
  Deploy:
166
  ```bash
 
180
  ```
181
 
182
  ### Modal API Deprecation Fixes Applied
183
+
184
+ During deployment I hit two Modal SDK deprecations and fixed them:
185
  1. `container_idle_timeout` β†’ `scaledown_window` (deprecated 2025-02-24)
186
  2. `allow_concurrent_inputs` β†’ `@modal.concurrent(max_inputs=10)` decorator (deprecated 2025-04-09)
187
 
188
  ### Budget: Separate $100 serving budget
189
+
190
+ This is separate from the ~$11.54 training budget I already spent. Serving costs:
191
  - A10G active: ~$5.04/hr
192
  - With scale-to-zero: ~$0.50-2.00/day typical
193
  - Health check endpoint at `/health` for monitoring
 
196
 
197
  ## 3. Gradio Model Switching β€” Backend Implemented, UI Deferred
198
 
199
+ ### Status: Backend ready, UI placement deferred to other agent
200
 
201
  ### Implemented: `core/llm_zerogpu_lora.py`
202
 
203
+ LoRA-aware ZeroGPU backend. It has the same API as `llm_zerogpu.py` (`chat_json`, `warm`, `backend_status`) but wraps the base model with `PeftModel.from_pretrained()` when `CHIEF_ENGINEER_LORA_REPO` is set. It is import-guarded β€” safe no-op if torch/transformers are absent.
 
 
204
 
205
  ### Implemented: `app.py` backend infrastructure
206
 
207
+ I added this to `app.py` (merged with UI agent's concurrent changes):
208
  - `MODEL_OPTIONS` list: "Base (Gemma 4 E4B)", "LoRA v2 (Standard E4B)", "LoRA v3 (QAT E4B)", "Modal API (remote)"
209
  - `MODEL_LORA_MAP` dict: maps UI labels β†’ HF Hub adapter repo IDs
210
  - `_apply_model_choice()` function: sets `CHIEF_ENGINEER_LORA_REPO` and `CHIEF_ENGINEER_BACKEND` env vars, reloads `core.llm` module
 
212
  - `core.llm_zerogpu_lora` imported at Space startup alongside `core.llm_zerogpu`
213
 
214
  ### UI placement rolled back
 
 
 
215
 
216
+ Per user request (another agent is handling the Gradio UI), I removed the dropdown widget placement and HTML note from `app.py`. The backend infrastructure remains so the UI agent can wire it in.
217
+
218
+ ### UI Agent Handoff (2026-06-14)
219
 
220
  **Already done (do NOT re-implement):**
221
+ - `core/llm_zerogpu_lora.py` β€” LoRA-aware ZeroGPU backend
222
+ - `app.py` β€” `_apply_model_choice()` function, `MODEL_OPTIONS` list, `MODEL_LORA_MAP` dict
223
+ - `app.py` β€” `build_job()` now accepts `model_choice` parameter
224
+ - `app.py` β€” `core.llm_zerogpu_lora` imported at startup
225
 
226
  **What the UI agent needs to do:**
227
  1. Add a `gr.Dropdown` with `MODEL_OPTIONS` choices in the STUDIO tab
228
  2. Wire `model_choice` into the `build_job` call in the event handler
229
+ 3. Add info line: "Local users: get LoRA models from HF Hub or `ollama pull`"
230
  4. `_apply_model_choice()` handles all backend switching automatically
231
 
232
  ---
 
248
  ## 5. Files Created/Modified
249
 
250
  | File | Action | Purpose |
251
+ |------|--------|---------|
252
  | `core/llm_zerogpu.py` | ✏️ Modified | E2Bβ†’E4B fix |
253
  | `core/llm_zerogpu_lora.py` | ✨ Created | LoRA-aware ZeroGPU backend |
254
  | `app.py` | ✏️ Modified | Add model selector dropdown + wiring |
scripts/export_deliberation.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export the Chief Engineer's multi-persona DELIBERATION as a HF-ready trace.
2
+
3
+ The lesson ledger (scripts/export_trace.py) shares *what the agent learned*. This
4
+ shares *how the agent thinks*: the turn-by-turn argument between the personas on
5
+ each job β€” O'Brien proposes, the Spine vetoes unsafe values, La Forge gives a
6
+ skeptical second opinion (and can dispute β†’ the operator overrides), the
7
+ deterministic world prints, La Forge grades each run, then delivers a run verdict.
8
+
9
+ Our own schema (one row per turn): session_id, track, turn, agent, role, act, stance,
10
+ content, + the job context (material/geometry/bed/env) so each row is self-describing.
11
+
12
+ Side-effect-free: runs against a throwaway ledger + policy in a temp dir, so the
13
+ shipped state is never touched. Offline-safe: with no LLM the personas fall back to
14
+ their deterministic voices, so the trace is fully reproducible.
15
+
16
+ Run: `make deliberation` (or `uv run python -m scripts.export_deliberation`) β†’ dist/deliberation/
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sys
22
+ import tempfile
23
+ from datetime import datetime, timedelta, timezone
24
+ from pathlib import Path
25
+
26
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # repo root on path
27
+
28
+ from core import inspector, seed_lessons
29
+ from core.chief_engineer import advise
30
+ from core.ledger import LedgerManager
31
+ from core.models import Advice, Environment, Job, PrintSettings
32
+ from core.spine import SpineValidator
33
+ from learn.loop import run_iteration
34
+ from learn.policy import LearnedPolicy
35
+
36
+ try: # ingestion is optional / removable (mirrors app.py)
37
+ from ingest.distill import reference_block
38
+ except Exception:
39
+ def reference_block(_material): # type: ignore
40
+ return []
41
+
42
+ DIST = Path(__file__).resolve().parent.parent / "dist" / "deliberation"
43
+ HF_REPO = "kylebrodeur/chief-engineer-deliberation"
44
+
45
+ # Representative jobs, chosen to exercise the full range of La Forge's stances
46
+ # (concur / caution / dispute→override) and the print-loop's climb to clean.
47
+ JOBS = [
48
+ ("ABS", "overhang", "edge", 26.0, 60.0, 4), # off-center ABS + thin fan β†’ dispute β†’ override
49
+ ("PETG", "overhang", "center", 24.0, 55.0, 3), # thin fan for an overhang β†’ caution
50
+ ("PLA", "adhesion", "center", 21.0, 45.0, 3), # inside sane bounds β†’ concur
51
+ ("TPU", "stringing", "edge", 23.0, 65.0, 3), # humid + short retraction β†’ caution
52
+ ]
53
+
54
+ ROLE = {
55
+ "O'Brien": "Chief Engineer",
56
+ "La Forge": "QA Inspector",
57
+ "Spine": "Safety Spine",
58
+ "World": "Outcome Simulator",
59
+ "Operator": "Operator",
60
+ }
61
+
62
+ CARD = """---
63
+ license: mit
64
+ task_categories: [text-generation]
65
+ language: [en]
66
+ tags: [3d-printing, additive-manufacturing, agent-trace, multi-agent, deliberation, build-small-hackathon]
67
+ pretty_name: Chief Engineer β€” Deliberation Traces
68
+ ---
69
+
70
+ # Chief Engineer β€” Deliberation Traces
71
+
72
+ Turn-by-turn **multi-persona deliberation** from **The Chief Engineer**, a small local
73
+ Gemma agent built for the HF Build Small hackathon (Backyard AI). Where the
74
+ [lesson ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
75
+ records *what the agent learned*, this records *how it reasons*: the argument between
76
+ the personas on each job. It grows two ways: a reproducible static export
77
+ (`make deliberation`) and **live turns logged on every run of the Space** (gated on
78
+ `HF_TOKEN`; config + agent reasoning only, never PII or uploaded files).
79
+
80
+ Each row is one **turn**:
81
+
82
+ - **O'Brien** (Chief Engineer) β€” proposes settings + reasoning over precedent.
83
+ - **Spine** (Safety Spine) β€” deterministically vetoes/clamps unsafe values.
84
+ - **La Forge** (QA Inspector) β€” a separate, skeptical voice: second opinion before the
85
+ print (`concur` / `caution` / `dispute`), a grade on each run, and a run verdict.
86
+ - **Operator** β€” the human, who can override a `dispute` and proceed.
87
+ - **World** (Outcome Simulator) β€” the deterministic physics-lite world that reports the
88
+ actual print outcome (the agent never grades its own work).
89
+
90
+ The integrity rule made literal: the proposer never marks its own homework.
91
+
92
+ ## Schema
93
+
94
+ `session_id, track, turn, agent, role, act, stance, content, material, geometry,
95
+ bed_position, env_temp, env_humidity, ts`
96
+
97
+ `track` is the phase β€” `preflight` (propose β†’ veto β†’ second opinion β†’ override),
98
+ `print-loop` (simulate β†’ grade, per iteration), `review` (run verdict).
99
+ """
100
+
101
+
102
+ def _settings_line(s: PrintSettings) -> str:
103
+ return (f"nozzle {s.nozzle_temp:.0f}Β°C, bed {s.bed_temp:.0f}Β°C, fan {s.fan_pct:.0f}%, "
104
+ f"first-layer fan {s.first_layer_fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm")
105
+
106
+
107
+ def export() -> Path:
108
+ DIST.mkdir(parents=True, exist_ok=True)
109
+ out = DIST / "deliberations.jsonl"
110
+
111
+ # throwaway state so the shipped ledger/policy are never mutated
112
+ tmp = Path(tempfile.mkdtemp(prefix="ce-delib-"))
113
+ ledger = LedgerManager(path=tmp / "ledger.jsonl")
114
+ seed_lessons.ensure_seeded(ledger)
115
+ spine = SpineValidator()
116
+
117
+ import json
118
+
119
+ clock = datetime(2026, 6, 14, 12, 0, 0, tzinfo=timezone.utc)
120
+ rows: list[dict] = []
121
+
122
+ def emit(job_id, track, turn, agent, act, content, *, ctx, stance=""):
123
+ nonlocal clock
124
+ clock += timedelta(seconds=7)
125
+ rows.append({
126
+ "session_id": job_id, "track": track, "turn": turn,
127
+ "agent": agent, "role": ROLE[agent], "act": act, "stance": stance,
128
+ "content": content.strip(),
129
+ "material": ctx["material"], "geometry": ctx["geometry"],
130
+ "bed_position": ctx["bed_position"],
131
+ "env_temp": ctx["env_temp"], "env_humidity": ctx["env_humidity"],
132
+ "ts": clock.isoformat(),
133
+ })
134
+
135
+ for material, geometry, bed, temp, hum, iters in JOBS:
136
+ job_id = f"{material}-{geometry}-{bed}".lower()
137
+ job = Job(geometry_type=geometry, material=material, bed_position=bed)
138
+ env = Environment(temp=temp, humidity=hum)
139
+ ctx = {"material": material, "geometry": geometry, "bed_position": bed,
140
+ "env_temp": temp, "env_humidity": hum}
141
+ # fresh policy per job so the loop's climb starts from baseline each time
142
+ policy = LearnedPolicy(path=tmp / f"policy-{job_id}.json")
143
+
144
+ # ── preflight: propose β†’ veto β†’ second opinion β†’ (override) ──
145
+ retrieved = ledger.retrieve(material, geometry, env.temp, env.humidity)
146
+ rec = advise(job, env, retrieved, reference_block(material),
147
+ policy.policy_note(material, geometry, env))
148
+ checked = spine.check(rec.advice.settings, material)
149
+ t = 1
150
+ emit(job_id, "preflight", t, "O'Brien", "propose",
151
+ f"{rec.advice.reasoning}\nProposed: {_settings_line(checked.settings)}.", ctx=ctx)
152
+ t += 1
153
+ emit(job_id, "preflight", t, "Spine", "veto",
154
+ ("Clamped: " + " Β· ".join(checked.vetoes)) if checked.vetoes
155
+ else "Within the safe envelope for this material β€” no clamp.", ctx=ctx,
156
+ stance="clamped" if checked.requires_approval else "clear")
157
+ t += 1
158
+ verdict = inspector.second_opinion(job, env, checked.settings, rec.advice)
159
+ emit(job_id, "preflight", t, "La Forge", "second_opinion",
160
+ f"{verdict.headline} β€” {verdict.detail}", ctx=ctx, stance=verdict.stance)
161
+ if verdict.stance.lower() == "dispute":
162
+ t += 1
163
+ emit(job_id, "preflight", t, "Operator", "override",
164
+ "Acknowledged La Forge's objection. Proceeding to print on the operator's call.",
165
+ ctx=ctx, stance="override")
166
+
167
+ # ── print-loop: simulate β†’ grade, per iteration ──
168
+ for n in range(1, iters + 1):
169
+ t += 1
170
+ r = run_iteration(job, env, policy, ledger, n, record=False)
171
+ clamp = " (Spine clamped a setting)" if r.clamped else ""
172
+ emit(job_id, "print-loop", t, "World", "simulate",
173
+ f"Iteration {n}: {r.result.detail}.{clamp} Policy: {r.learned}.", ctx=ctx,
174
+ stance=r.result.outcome)
175
+ t += 1
176
+ g = inspector.grade_iteration(geometry, r.result)
177
+ emit(job_id, "print-loop", t, "La Forge", "grade",
178
+ f"{g.headline} β€” {g.detail}", ctx=ctx, stance=g.stance)
179
+
180
+ # ── review: one verdict across the run ──
181
+ # rebuild records for the summary from a fresh deterministic pass
182
+ sess_records = []
183
+ rpolicy = LearnedPolicy(path=tmp / f"policy-rev-{job_id}.json")
184
+ for n in range(1, iters + 1):
185
+ sess_records.append(run_iteration(job, env, rpolicy, ledger, n, record=False))
186
+ summary = inspector.summarize_run(sess_records, material=material, geometry=geometry)
187
+ t += 1
188
+ emit(job_id, "review", t, "La Forge", "verdict",
189
+ f"{summary.headline} β€” {summary.detail}", ctx=ctx, stance=summary.stance)
190
+
191
+ with out.open("w", encoding="utf-8") as f:
192
+ for row in rows:
193
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
194
+ (DIST / "README.md").write_text(CARD, encoding="utf-8")
195
+
196
+ jobs = len({r["session_id"] for r in rows})
197
+ print(f"exported {len(rows)} turns across {jobs} jobs β†’ {out}")
198
+ print(f"dataset card β†’ {DIST / 'README.md'}")
199
+ print(f"publish: hf upload {HF_REPO} {DIST} . --repo-type dataset")
200
+ return out
201
+
202
+
203
+ if __name__ == "__main__":
204
+ export()