kylebrodeur commited on
Commit
eb62e9b
Β·
verified Β·
1 Parent(s): e9c4780

deploy: update Space from deploy_preflight --push

Browse files
.codeboarding/.codeboardingignore ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CodeBoarding Ignore File
2
+ # Add patterns here for files and directories that should be excluded from CodeBoarding analysis.
3
+ # Use the same format as .gitignore (gitignore syntax / gitwildmatch patterns).
4
+ #
5
+ # To stop ignoring a pattern, prefix it with ! (e.g., !important_file.txt)
6
+ #
7
+ # NOTE: The following are ALWAYS excluded (not configurable):
8
+ # - Hidden directories (starting with .)
9
+ # - .git/, .codeboarding/, node_modules/, __pycache__/
10
+ # - Build output: build/, dist/, coverage/
11
+ #
12
+ # This file is automatically loaded by CodeBoarding analysis tools to exclude
13
+ # specified paths from code analysis, architecture generation, and other processing.
14
+
15
+ # ============================================================================
16
+ # Ignored directories (customizable β€” remove lines to include them)
17
+ # ============================================================================
18
+
19
+ # Python virtual environments
20
+ venv/
21
+ env/
22
+ *.egg-info/
23
+
24
+ # Java (Maven/Gradle) and Rust (Cargo) build output. Both ecosystems
25
+ # produce a top-level ``target/`` directory full of compiled artifacts β€”
26
+ # kept here as well as in ``_ALWAYS_IGNORED_DIRS`` so users who customize
27
+ # their ``.codeboardingignore`` continue to skip it even after edits.
28
+ target/
29
+ bin/
30
+ out/
31
+
32
+ # .NET / C# build output
33
+ obj/
34
+
35
+ # Go
36
+ vendor/
37
+ testdata/
38
+
39
+ # PHP
40
+ cache/
41
+
42
+ # Custom
43
+ temp/
44
+ repos/
45
+ runs/
46
+
47
+ # ============================================================================
48
+ # Test and infrastructure files
49
+ # ============================================================================
50
+
51
+ # Test directories
52
+ **/__tests__/**
53
+ **/tests/**
54
+ **/test/**
55
+ **/__test__/**
56
+ **/testing/**
57
+ **/testutil/**
58
+
59
+ # Java/Kotlin test directories (Maven/Gradle structure)
60
+ **/src/test/**
61
+ **/src/testFixtures/**
62
+ **/src/integration-test/**
63
+ **/src/jmh/**
64
+ **/src/contractTest/**
65
+ **/osgi-tests/**
66
+
67
+ # Test files by naming convention
68
+ *.test.*
69
+ *.spec.*
70
+ *_test.*
71
+ *test_*.py
72
+ test_*.py
73
+ *Test.java
74
+ *IT.java
75
+ *Test.kt
76
+ *IT.kt
77
+ *Tests.java
78
+
79
+ # Mock, fixture, and stub directories
80
+ **/__mocks__/**
81
+ **/mocks/**
82
+ **/fixtures/**
83
+ **/fixture/**
84
+ **/stubs/**
85
+ **/stub/**
86
+ **/fakes/**
87
+ **/fake/**
88
+
89
+ # E2E and integration test directories
90
+ **/e2e/**
91
+ **/integration-tests/**
92
+ **/integration_test*/**
93
+
94
+ # ============================================================================
95
+ # Non-production code
96
+ # ============================================================================
97
+
98
+ # Example and documentation code
99
+ **/examples/**
100
+ **/documentation/examples/**
101
+
102
+ # Generated code
103
+ *.pb.go
104
+ **/generated_parser*
105
+
106
+ # Java/Kotlin metadata files
107
+ module-info.java
108
+
109
+ # ============================================================================
110
+ # Build artifacts and minified files
111
+ # ============================================================================
112
+
113
+ *.bundle.js
114
+ *.bundle.js.map
115
+ *.min.js
116
+ *.min.css
117
+ *.chunk.js
118
+ *.chunk.js.map
119
+
120
+ # ============================================================================
121
+ # Build tool configs and infrastructure
122
+ # ============================================================================
123
+
124
+ esbuild*
125
+ webpack*
126
+ rollup*
127
+ vite.config.*
128
+ gulpfile*
129
+ gruntfile*
130
+ *.config.*
.codeboarding/logs/wrapper-server.log ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [stderr] INFO: Started server process [95234]
2
+ [stderr] INFO: Waiting for application startup.
3
+ [stderr] INFO: Application startup complete.
4
+ [stderr] INFO: Uvicorn running on http://127.0.0.1:8765 (Press CTRL+C to quit)
5
+ INFO: 127.0.0.1:50676 - "GET /health HTTP/1.1" 200 OK
6
+ [stderr] INFO: ('127.0.0.1', 50686) - "WebSocket /ws" [accepted]
7
+ [stderr] 2026-06-13 13:34:27 INFO [codeboarding_pro.ws.server:226] WebSocket connected: session ce9a8523-69cd-43d2-81c9-3cce7f7414f9
8
+ [stderr] INFO: connection open
9
+ [stderr] 2026-06-13 13:34:27 INFO [tool_registry.installers:451] Installing Node.js packages: ['pyright@1.1.400', 'typescript-language-server@4.3.4', 'typescript@5.7', 'intelephense@1.16.5']
10
+ [stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:465] Node.js packages installed successfully
11
+ [stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:225] tokei: already installed, skipping
12
+ [stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:225] gopls: already installed, skipping
13
+ [stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:225] rust-analyzer: already installed, skipping
14
+ [stderr] 2026-06-13 13:34:29 INFO [tool_registry.installers:489] java already installed
15
+ [stderr] 2026-06-13 13:34:29 WARNING [tool_registry.installers:367] csharp-ls: dotnet not found on PATH; skipping install. Users must install it before running analysis.
16
+ [stderr] 2026-06-13 13:34:29 INFO [static_analyzer.java_utils:125] Found Java 21 at /usr/lib/jvm/java-21-openjdk-amd64
17
+ [stderr] 2026-06-13 13:34:29 INFO [codeboarding_pro.lsp.bootstrap:56] LSP startup attempt 1/3
18
+ [stderr] 2026-06-13 13:34:29 INFO [codeboarding_pro.session:185] Session ce9a8523-69cd-43d2-81c9-3cce7f7414f9 initialized: repo=/home/kylebrodeur/projects/microfactory-lab/chief-engineer, project=chief-engineer, output=/home/kylebrodeur/projects/microfactory-lab/chief-engineer/.codeboarding
19
+ [stderr] 2026-06-13 13:34:29 INFO [static_analyzer:227] Starting engine LSP client for Python at /home/kylebrodeur/projects/microfactory-lab/chief-engineer
20
+ [stderr] 2026-06-13 13:34:30 INFO [static_analyzer:252] Python LSP start: 0.3s
21
+ [stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.file_monitor:106] FileMonitor started for /home/kylebrodeur/projects/microfactory-lab/chief-engineer
22
+ [stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.session:203] Session ce9a8523-69cd-43d2-81c9-3cce7f7414f9: FileMonitor activated
23
+ [stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.head_watcher:48] HeadWatcher started on /home/kylebrodeur/projects/microfactory-lab/.git/HEAD
24
+ [stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.session:248] Session ce9a8523-69cd-43d2-81c9-3cce7f7414f9: LSP wiring complete
25
+ [stderr] 2026-06-13 13:34:30 INFO [codeboarding_pro.lsp.bootstrap:67] LSP clients started successfully (attempt 1)
.gitattributes CHANGED
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/benchy.glb filter=lfs diff=lfs merge=lfs -text
37
  a5c557efbc56.cap/content/segments/segment-0/display.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/benchy.glb filter=lfs diff=lfs merge=lfs -text
37
  a5c557efbc56.cap/content/segments/segment-0/display.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ 3ca9c96d1d92.cap/content/segments/segment-0/display.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ 3ca9c96d1d92.cap/screenshots/display.jpg filter=lfs diff=lfs merge=lfs -text
3ca9c96d1d92.cap/content/cursors/cursor_0.png ADDED
3ca9c96d1d92.cap/content/segments/segment-0/cursor.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "clicks": [],
3
+ "moves": []
4
+ }
3ca9c96d1d92.cap/content/segments/segment-0/display.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:663a6075dcceca04cc0843d031353ed1110a8008de58fb6a16fb81e15cae7774
3
+ size 5982983
3ca9c96d1d92.cap/content/segments/segment-0/keyboard.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e5cc23716d9b28b4661a952cb0f768a4babdcd0a0b5eac73110d2fac232bef41
3
+ size 7
3ca9c96d1d92.cap/project-config.json ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "aspectRatio": null,
3
+ "background": {
4
+ "source": {
5
+ "type": "color",
6
+ "value": [
7
+ 255,
8
+ 255,
9
+ 255
10
+ ],
11
+ "alpha": 255
12
+ },
13
+ "blur": 0.0,
14
+ "padding": 0.0,
15
+ "rounding": 0.0,
16
+ "roundingType": "squircle",
17
+ "inset": 0,
18
+ "crop": null,
19
+ "shadow": 73.6,
20
+ "advancedShadow": {
21
+ "size": 14.4,
22
+ "opacity": 68.1,
23
+ "blur": 3.8
24
+ },
25
+ "border": null
26
+ },
27
+ "camera": {
28
+ "hide": false,
29
+ "mirror": false,
30
+ "position": {
31
+ "x": "right",
32
+ "y": "bottom"
33
+ },
34
+ "size": 30.0,
35
+ "zoomSize": 60.0,
36
+ "rounding": 100.0,
37
+ "shadow": 62.5,
38
+ "advancedShadow": {
39
+ "size": 33.9,
40
+ "opacity": 44.2,
41
+ "blur": 10.5
42
+ },
43
+ "shape": "square",
44
+ "roundingType": "squircle",
45
+ "scaleDuringZoom": 0.7,
46
+ "backgroundBlur": {
47
+ "mode": "off"
48
+ }
49
+ },
50
+ "audio": {
51
+ "mute": false,
52
+ "improve": false,
53
+ "micVolumeDb": 0.0,
54
+ "micStereoMode": "stereo",
55
+ "systemVolumeDb": 0.0
56
+ },
57
+ "cursor": {
58
+ "hide": false,
59
+ "hideWhenIdle": false,
60
+ "hideWhenIdleDelay": 2.0,
61
+ "size": 100,
62
+ "type": "auto",
63
+ "animationStyle": "mellow",
64
+ "tension": 470.0,
65
+ "mass": 3.0,
66
+ "friction": 70.0,
67
+ "raw": false,
68
+ "motionBlur": 0.5,
69
+ "useSvg": true,
70
+ "rotationAmount": 0.15,
71
+ "baseRotation": 0.0,
72
+ "clickSpring": null,
73
+ "stopMovementInLastSeconds": null
74
+ },
75
+ "hotkeys": {
76
+ "show": false
77
+ },
78
+ "timeline": {
79
+ "segments": [
80
+ {
81
+ "recordingSegment": 0,
82
+ "timescale": 1.0,
83
+ "start": 0.0,
84
+ "end": 69.656689
85
+ }
86
+ ],
87
+ "zoomSegments": [],
88
+ "sceneSegments": [],
89
+ "maskSegments": [],
90
+ "textSegments": [],
91
+ "captionSegments": [],
92
+ "keyboardSegments": []
93
+ },
94
+ "captions": null,
95
+ "keyboard": null,
96
+ "clips": [
97
+ {
98
+ "index": 0,
99
+ "offsets": {
100
+ "camera": 0.0,
101
+ "mic": 0.0,
102
+ "system_audio": 0.0
103
+ }
104
+ }
105
+ ],
106
+ "annotations": [],
107
+ "screenMotionBlur": 0.5,
108
+ "screenMovementSpring": {
109
+ "stiffness": 200.0,
110
+ "damping": 40.0,
111
+ "mass": 2.25
112
+ }
113
+ }
3ca9c96d1d92.cap/recording-meta.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "platform": "Windows",
3
+ "pretty_name": "Cap 2026-06-13 at 13.15.50",
4
+ "sharing": null,
5
+ "segments": [
6
+ {
7
+ "display": {
8
+ "path": "content/segments/segment-0/display.mp4",
9
+ "fps": 33,
10
+ "start_time": 0.2475809
11
+ },
12
+ "cursor": "content/segments/segment-0/cursor.json",
13
+ "keyboard": "content/segments/segment-0/keyboard.bin"
14
+ }
15
+ ],
16
+ "cursors": {
17
+ "0": {
18
+ "imagePath": "content/cursors/cursor_0.png",
19
+ "hotspot": {
20
+ "x": 0.34375,
21
+ "y": 0.3125
22
+ },
23
+ "shape": "Windows|Hand"
24
+ }
25
+ },
26
+ "status": {
27
+ "status": "Complete"
28
+ }
29
+ }
3ca9c96d1d92.cap/screenshots/display.jpg ADDED

Git LFS Details

  • SHA256: 657388569bbebcf70d6aff8ff956763994bc23f6681b6ee8d0e496b021c3abf9
  • Pointer size: 131 Bytes
  • Size of remote file: 197 kB
Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: setup setup-zerogpu assets run test demo bench trace preflight record record-check
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
@@ -23,9 +23,12 @@ test: ## headless core tests (no Ollama required)
23
  preflight: ## GO/NO-GO gate on the real stack (see RUNBOOK)
24
  uv run python -m scripts.preflight
25
 
26
- deploy-check: ## deploy/record readiness gate (offline: build + files + creds + Space)
27
  uv run python -m scripts.deploy_preflight
28
 
 
 
 
29
  demo: ## scripted integration run / video-beat dry run
30
  uv run python -m scripts.scripted_demo
31
 
@@ -41,5 +44,8 @@ record-check: ## recording preflight (cap-cli + Space + playwright gates)
41
  record: ## full recording (manual mode): preflight β†’ cap β†’ beat cues β†’ export mp4
42
  uv run python -m scripts.record
43
 
44
- record-auto: ## recording with Playwright auto-driver (WSL only, may crash)
 
 
 
45
  uv run python -m scripts.record --mode auto
 
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
 
23
  preflight: ## GO/NO-GO gate on the real stack (see RUNBOOK)
24
  uv run python -m scripts.preflight
25
 
26
+ deploy-check: ## deploy/record readiness gate (offline: build + files + creds + Space + dataset)
27
  uv run python -m scripts.deploy_preflight
28
 
29
+ deploy: ## run the gates, then UPDATE the Space files (hf upload) + factory reboot (needs HF_TOKEN)
30
+ uv run python -m scripts.deploy_preflight --push
31
+
32
  demo: ## scripted integration run / video-beat dry run
33
  uv run python -m scripts.scripted_demo
34
 
 
44
  record: ## full recording (manual mode): preflight β†’ cap β†’ beat cues β†’ export mp4
45
  uv run python -m scripts.record
46
 
47
+ record-cues: ## beat cues only (no cap) β€” you record with Cap desktop at high quality
48
+ uv run python -m scripts.record --mode cues
49
+
50
+ record-auto: ## recording with Playwright auto-driver (WSL only)
51
  uv run python -m scripts.record --mode auto
README.md CHANGED
@@ -139,7 +139,7 @@ Astrometrics UI lands (see `../DESIGN.md`). **Not** Well-Tuned β€” fine-tuning i
139
  - **Ledger dataset:** [kylebrodeur/chief-engineer-ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
140
  - **Demo video:** [<!-- TODO: add video URL after recording -->]()
141
  - **Social post:** [<!-- TODO: add social post URL after publishing -->]()
142
- - **Judge's 2-min tour:** [`docs/writeup/JUDGE-GUIDE.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/writeup/JUDGE-GUIDE.md)
143
  - **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
144
 
145
  ## License
 
139
  - **Ledger dataset:** [kylebrodeur/chief-engineer-ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
140
  - **Demo video:** [<!-- TODO: add video URL after recording -->]()
141
  - **Social post:** [<!-- TODO: add social post URL after publishing -->]()
142
+ - **How to use it (guided tour):** [`docs/RUNBOOK.md` Β§2](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md#2--use-the-tool-the-guided-tour--also-the-judges-tour)
143
  - **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
144
 
145
  ## License
app.py CHANGED
@@ -293,7 +293,8 @@ def second_opinion(state):
293
  advice = Advice(**state["advice"])
294
  verdict = inspector.second_opinion(job, env, settings, advice)
295
  field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
296
- "stance": verdict.stance, "headline": verdict.headline})
 
297
  panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
298
  if verdict.stance.lower() == "dispute":
299
  panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
@@ -345,7 +346,7 @@ def run_print(state, iterations):
345
  "env_temp": env.temp, "env_humidity": env.humidity,
346
  "iterations": len(sess.records), "q_start": round(traj[0], 3),
347
  "q_end": round(traj[-1], 3), "first_clean": first,
348
- "verdict": run_summary.stance})
349
  headline = (
350
  f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β€” "
351
  f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
 
293
  advice = Advice(**state["advice"])
294
  verdict = inspector.second_opinion(job, env, settings, advice)
295
  field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
296
+ "inspector_stance": verdict.stance,
297
+ "inspector_headline": verdict.headline})
298
  panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
299
  if verdict.stance.lower() == "dispute":
300
  panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
 
346
  "env_temp": env.temp, "env_humidity": env.humidity,
347
  "iterations": len(sess.records), "q_start": round(traj[0], 3),
348
  "q_end": round(traj[-1], 3), "first_clean": first,
349
+ "inspector_stance": run_summary.stance})
350
  headline = (
351
  f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β€” "
352
  f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
core/field_log.py CHANGED
@@ -62,56 +62,28 @@ def is_active() -> bool:
62
  return _get_scheduler() is not None
63
 
64
 
65
- def log_build(job: dict, env: dict, settings: dict, advice: dict,
66
- backend: str, used_fallback: bool) -> bool:
67
- """Append one BUILD interaction row. Returns True if logged."""
68
- sched = _get_scheduler()
69
- if sched is None:
70
- return False
71
-
72
- row = {
73
- "ts": datetime.now(timezone.utc).isoformat(),
74
- "material": job.get("material"),
75
- "geometry": job.get("geometry_type"),
76
- "env_temp": env.get("temp"),
77
- "env_humidity": env.get("humidity"),
78
- "bed_position": job.get("bed_position"),
79
- "printer": job.get("printer", "Creality Ender 3 V2"),
80
- "backend": backend,
81
- "used_fallback": used_fallback,
82
- "settings": {
83
- k: v for k, v in settings.items()
84
- if k in ("nozzle_temp", "bed_temp", "retraction_mm", "fan_pct",
85
- "first_layer_fan_pct", "print_speed")
86
- },
87
- "risks": advice.get("risks", []),
88
- "inspector_verdict": None, # filled later if visitor runs Second Opinion
89
- "simulated_outcome": None, # filled later if visitor clicks Simulate
90
- }
91
-
92
- with _lock:
93
- with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
94
- f.write(json.dumps(row, ensure_ascii=False) + "\n")
95
-
96
- # Tell the scheduler we have new data (it normally scans on a timer,
97
- # but this nudges it to notice the append).
98
- try:
99
- sched.trigger()
100
- except Exception:
101
- pass # trigger is best-effort; the timer will catch it
102
-
103
- return True
104
-
105
-
106
- def log_event(kind: str, payload: dict) -> bool:
107
- """Append one interaction row of any KIND β€” build | second_opinion | simulate |
108
- record | print_run β€” so EVERY run (not just BUILD) lands in the shared dataset.
109
- Same gate (HF_TOKEN) + privacy rules: config/outcomes only, never PII or files."""
110
  try:
111
  sched = _get_scheduler()
112
  if sched is None:
113
  return False
114
- row = {"ts": datetime.now(timezone.utc).isoformat(), "kind": kind, **payload}
 
 
115
  with _lock:
116
  with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
117
  f.write(json.dumps(row, ensure_ascii=False) + "\n")
@@ -124,6 +96,32 @@ def log_event(kind: str, payload: dict) -> bool:
124
  return False # logging is best-effort β€” never break a run
125
 
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  def privacy_notice() -> str:
128
  """One-line UI disclosure, shown only when logging is active."""
129
  return (
 
62
  return _get_scheduler() is not None
63
 
64
 
65
+ # Canonical FLAT schema β€” every row carries exactly these keys (None when N/A), all
66
+ # scalars/strings (no nested dicts/lists). This is what makes the HF dataset viewer
67
+ # render cleanly: a rectangular, well-typed table instead of ragged/nested JSON.
68
+ _CANON = (
69
+ "ts", "kind", "material", "geometry", "env_temp", "env_humidity",
70
+ "bed_position", "printer", "backend", "used_fallback",
71
+ "nozzle_temp", "bed_temp", "fan_pct", "retraction_mm", "first_layer_fan_pct",
72
+ "risks", "risk_count", "inspector_stance", "inspector_headline", "agreement",
73
+ "outcome", "quality", "iterations", "q_start", "q_end", "first_clean",
74
+ )
75
+
76
+
77
+ def _write_row(fields: dict) -> bool:
78
+ """Normalize to the canonical flat schema (drop unknown keys, fill missing with
79
+ None) and append one JSONL line. Gated + exception-safe β€” never breaks a run."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  try:
81
  sched = _get_scheduler()
82
  if sched is None:
83
  return False
84
+ row = {k: None for k in _CANON}
85
+ row.update({k: v for k, v in fields.items() if k in _CANON})
86
+ row["ts"] = datetime.now(timezone.utc).isoformat()
87
  with _lock:
88
  with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
89
  f.write(json.dumps(row, ensure_ascii=False) + "\n")
 
96
  return False # logging is best-effort β€” never break a run
97
 
98
 
99
+ def log_event(kind: str, payload: dict) -> bool:
100
+ """Append one interaction row of any KIND β€” build | second_opinion | simulate |
101
+ record | print_run β€” normalized to the canonical flat schema. Same gate (HF_TOKEN)
102
+ + privacy rules: config/outcomes only, never PII or files."""
103
+ return _write_row({**payload, "kind": kind})
104
+
105
+
106
+ def log_build(job: dict, env: dict, settings: dict, advice: dict,
107
+ backend: str, used_fallback: bool) -> bool:
108
+ """Append one BUILD row (flattened settings + risks-as-string for the viewer)."""
109
+ risks = advice.get("risks", []) or []
110
+ return _write_row({
111
+ "kind": "build",
112
+ "material": job.get("material"), "geometry": job.get("geometry_type"),
113
+ "env_temp": env.get("temp"), "env_humidity": env.get("humidity"),
114
+ "bed_position": job.get("bed_position"),
115
+ "printer": job.get("printer", "Creality Ender 3 V2"),
116
+ "backend": backend, "used_fallback": used_fallback,
117
+ "nozzle_temp": settings.get("nozzle_temp"), "bed_temp": settings.get("bed_temp"),
118
+ "fan_pct": settings.get("fan_pct"), "retraction_mm": settings.get("retraction_mm"),
119
+ "first_layer_fan_pct": settings.get("first_layer_fan_pct"),
120
+ "risks": ", ".join(str(r.get("risk")) for r in risks if isinstance(r, dict)) or None,
121
+ "risk_count": len(risks),
122
+ })
123
+
124
+
125
  def privacy_notice() -> str:
126
  """One-line UI disclosure, shown only when logging is active."""
127
  return (
scripts/deploy_preflight.py CHANGED
@@ -16,6 +16,7 @@ GO β†’ safe to `hf upload` + reboot + record. Run: make deploy-check
16
  from __future__ import annotations
17
 
18
  import json
 
19
  import os
20
  import re
21
  import shutil
@@ -25,6 +26,14 @@ from pathlib import Path
25
 
26
  ROOT = Path(__file__).resolve().parent.parent
27
  SPACE = "build-small-hackathon/microfactory-lab"
 
 
 
 
 
 
 
 
28
  _fail: list[str] = []
29
  _warn: list[str] = []
30
 
@@ -208,7 +217,59 @@ def d9_space(authed: bool) -> None:
208
  warn("D9 space", f"could not query Space: {e!r}")
209
 
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  def main() -> None:
 
 
 
 
 
 
212
  print("Deploy / record preflight β€” " + SPACE)
213
  print("=" * 70)
214
  d1_build()
@@ -221,16 +282,24 @@ def main() -> None:
221
  d7_data()
222
  authed = d8_credentials()
223
  d9_space(authed)
 
224
  print("=" * 70)
225
  if _fail:
226
  print(f"πŸ”΄ NO-GO: fix {len(_fail)} blocker(s) β€” {', '.join(_fail)}")
227
  sys.exit(1)
 
 
 
 
 
 
228
  if _warn:
229
  print(f"🟑 GO with warnings ({', '.join(_warn)}) β€” read them; credentials/Space "
230
- "warnings just mean 'deploy from an authenticated machine'.")
 
231
  sys.exit(0)
232
  print("🟒 GO β€” local build clean, files + frontmatter ready, authenticated. "
233
- "hf upload β†’ --factory-reboot β†’ smoke-test β†’ record.")
234
 
235
 
236
  if __name__ == "__main__":
 
16
  from __future__ import annotations
17
 
18
  import json
19
+ import argparse
20
  import os
21
  import re
22
  import shutil
 
26
 
27
  ROOT = Path(__file__).resolve().parent.parent
28
  SPACE = "build-small-hackathon/microfactory-lab"
29
+ FIELD_LOG_DATASET = "build-small-hackathon/chief-engineer-field-log"
30
+ # Uploaded to the Space = everything EXCEPT these (keeps learn/ + assets/ + data/*.jsonl,
31
+ # which the app imports/needs; drops docs, spikes, secrets, caches, runtime/transient files).
32
+ SPACE_IGNORE = [
33
+ "docs/**", "spike/**", "field_logs/**", ".venv/**", "**/__pycache__/**",
34
+ "**/*.pyc", ".git/**", ".env", "data/policy.json", "data/_generated.glb",
35
+ "data/_vprint.gif", "uv.lock", ".pytest_cache/**",
36
+ ]
37
  _fail: list[str] = []
38
  _warn: list[str] = []
39
 
 
217
  warn("D9 space", f"could not query Space: {e!r}")
218
 
219
 
220
+ # ── D10 Β· the field-log dataset is set + reachable (Sharing-is-Caring / all-runs) ──
221
+ def d10_dataset(authed: bool) -> None:
222
+ if not authed:
223
+ warn("D10 dataset", "skipped β€” no credentials to verify the field-log dataset")
224
+ return
225
+ try:
226
+ from huggingface_hub import HfApi
227
+ api = HfApi()
228
+ if api.repo_exists(FIELD_LOG_DATASET, repo_type="dataset"):
229
+ files = api.list_repo_files(FIELD_LOG_DATASET, repo_type="dataset")
230
+ logged = any(f.endswith("interactions.jsonl") for f in files)
231
+ ok("D10 dataset", f"{FIELD_LOG_DATASET} exists"
232
+ + (" Β· interactions.jsonl present (runs are logging)" if logged
233
+ else " Β· no interactions.jsonl yet (do one BUILD on the Space to confirm)"))
234
+ else:
235
+ warn("D10 dataset", f"{FIELD_LOG_DATASET} not found β€” create it, or let CommitScheduler "
236
+ "make it on first run (needs HF_TOKEN as a Space secret).")
237
+ except Exception as e: # noqa: BLE001
238
+ warn("D10 dataset", f"could not verify dataset: {e!r}")
239
+
240
+
241
+ # ── push: actually update the Space files (gated on green + auth) ─────────────
242
+ def push_space(factory_reboot: bool = True) -> None:
243
+ """Upload the app to the Space (everything except SPACE_IGNORE) and reboot.
244
+ Only runs after the gates pass + credentials are present."""
245
+ try:
246
+ from huggingface_hub import HfApi
247
+ except Exception as e: # noqa: BLE001
248
+ fail("PUSH", f"huggingface_hub unavailable: {e!r}")
249
+ return
250
+ api = HfApi()
251
+ print(f"\n⏫ uploading {ROOT.name}/ β†’ {SPACE} (excluding docs, spike, caches, secrets)…")
252
+ try:
253
+ api.upload_folder(repo_id=SPACE, repo_type="space", folder_path=str(ROOT),
254
+ ignore_patterns=SPACE_IGNORE,
255
+ commit_message="deploy: update Space from deploy_preflight --push")
256
+ ok("PUSH", "files uploaded")
257
+ if factory_reboot:
258
+ api.restart_space(SPACE, factory_reboot=True)
259
+ ok("PUSH", "factory reboot requested β€” Space rebuilding (~1-2 min)")
260
+ print(" Next: wait for build, then smoke-test (BUILD shows reasoning not Error; "
261
+ "O'Brien/La Forge; reset button; wide UI).")
262
+ except Exception as e: # noqa: BLE001
263
+ fail("PUSH", f"upload/restart failed: {e!r}")
264
+
265
+
266
  def main() -> None:
267
+ ap = argparse.ArgumentParser(description="Deploy/record readiness gate (+ optional Space push).")
268
+ ap.add_argument("--push", action="store_true",
269
+ help="after the gates pass, UPDATE the Space files (hf upload) + factory reboot")
270
+ ap.add_argument("--no-reboot", action="store_true", help="with --push, skip the factory reboot")
271
+ args = ap.parse_args()
272
+
273
  print("Deploy / record preflight β€” " + SPACE)
274
  print("=" * 70)
275
  d1_build()
 
282
  d7_data()
283
  authed = d8_credentials()
284
  d9_space(authed)
285
+ d10_dataset(authed)
286
  print("=" * 70)
287
  if _fail:
288
  print(f"πŸ”΄ NO-GO: fix {len(_fail)} blocker(s) β€” {', '.join(_fail)}")
289
  sys.exit(1)
290
+ if args.push:
291
+ if not authed:
292
+ print("πŸ”΄ --push needs HF credentials (HF_TOKEN or `hf auth login`). Nothing pushed.")
293
+ sys.exit(1)
294
+ push_space(factory_reboot=not args.no_reboot)
295
+ sys.exit(1 if _fail else 0)
296
  if _warn:
297
  print(f"🟑 GO with warnings ({', '.join(_warn)}) β€” read them; credentials/Space "
298
+ "warnings just mean 'deploy from an authenticated machine'. "
299
+ "Run with --push (authenticated) to update the Space.")
300
  sys.exit(0)
301
  print("🟒 GO β€” local build clean, files + frontmatter ready, authenticated. "
302
+ "Re-run with --push to update the Space + reboot, then smoke-test β†’ record.")
303
 
304
 
305
  if __name__ == "__main__":
scripts/record.py CHANGED
@@ -39,6 +39,8 @@ EXPORT_DIR_WIN = "D:\\workspace\\recordings" # must exist from Windows side
39
  EXPORT_DIR_WSL = "/mnt/d/workspace/recordings" # WSL equivalent
40
  SLOWMO_DEFAULT = 400 # ms between Playwright actions
41
  CAP_FPS = "60" # recording quality (1-120)
 
 
42
 
43
 
44
  # ---------------------------------------------------------------------------
@@ -472,21 +474,29 @@ MANUAL_BEATS: dict[str, list[dict]] = {
472
  # record
473
  # ---------------------------------------------------------------------------
474
 
475
- def record_manual(beat_name: str, url: str = SPACE_URL) -> Path | None:
476
- """Manual mode: start cap, print beat cues with countdowns, stop cap, export.
477
- You drive the Windows browser β€” Playwright stays out of it."""
 
478
  print(f"\n=== RECORD (manual): beat '{beat_name}' ===\n")
479
 
480
- # 1) Start detached Cap recording
481
- print(" starting Cap recording (detached)...")
482
- screen_id = _get_screen_id()
483
- started = _cap_json("record", "start", "--screen", screen_id, "--detach", "--json")
484
- if not started or "recordingId" not in started:
485
- print(" βœ— failed to start recording")
486
- return None
487
- rec_id = started["recordingId"]
488
- cap_path = started.get("path", "unknown")
489
- print(f" βœ“ recording started (id={rec_id})")
 
 
 
 
 
 
 
490
 
491
  # 2) Print beat cues β€” user drives the browser
492
  beats = MANUAL_BEATS.get(beat_name, MANUAL_BEATS["all"])
@@ -512,6 +522,11 @@ def record_manual(beat_name: str, url: str = SPACE_URL) -> Path | None:
512
  time.sleep(1)
513
  print(f" βœ“ cut{' ' * 20}")
514
 
 
 
 
 
 
515
  # 3) Stop recording
516
  print("\n stopping recording...")
517
  stopped = _cap_json("record", "stop", "--id", rec_id, "--json")
@@ -524,7 +539,7 @@ def record_manual(beat_name: str, url: str = SPACE_URL) -> Path | None:
524
  ts = time.strftime("%Y%m%d-%H%M%S")
525
  out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
526
  print(f" exporting to {out_win} ...")
527
- export_result = _cap("export", cap_path, "--output", out_win, "--json")
528
  if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
529
  print(f" βœ“ exported β†’ {out_win}")
530
  return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
@@ -559,6 +574,10 @@ def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL)
559
  page = browser.new_page()
560
  page.set_viewport_size({"width": 1707, "height": 1067}) # native screen res
561
 
 
 
 
 
562
  # Dismiss Chrome restore bubble + any initial popups
563
  time.sleep(1.0)
564
  _dismiss_popups(page)
@@ -610,7 +629,7 @@ def record_auto(beat_name: str, slowmo: int, pause: float, url: str = SPACE_URL)
610
  ts = time.strftime("%Y%m%d-%H%M%S")
611
  out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
612
  print(f" exporting to {out_win} ...")
613
- export_result = _cap("export", cap_path, "--output", out_win, "--json")
614
  if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
615
  print(f" βœ“ exported β†’ {out_win}")
616
  return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
@@ -629,8 +648,8 @@ def main() -> None:
629
  )
630
  ap.add_argument("--beat", default="all", choices=sorted(MANUAL_BEATS),
631
  help="which beat(s) to record (default: all)")
632
- ap.add_argument("--mode", default="manual", choices=["manual", "auto"],
633
- help="manual=you drive the Windows browser (default); auto=Playwright (WSL only, unreliable)")
634
  ap.add_argument("--preflight-only", action="store_true",
635
  help="run preflight checks and exit")
636
  ap.add_argument("--slowmo", type=int, default=SLOWMO_DEFAULT,
@@ -652,6 +671,8 @@ def main() -> None:
652
 
653
  if args.mode == "auto":
654
  result = record_auto(args.beat, args.slowmo, args.pause, url)
 
 
655
  else:
656
  result = record_manual(args.beat, url)
657
 
 
39
  EXPORT_DIR_WSL = "/mnt/d/workspace/recordings" # WSL equivalent
40
  SLOWMO_DEFAULT = 400 # ms between Playwright actions
41
  CAP_FPS = "60" # recording quality (1-120)
42
+ EXPORT_QUALITY = "maximum" # mp4 compression: maximum, social, web, potato
43
+ EXPORT_RES = "1707x1067" # native screen resolution
44
 
45
 
46
  # ---------------------------------------------------------------------------
 
474
  # record
475
  # ---------------------------------------------------------------------------
476
 
477
+ def record_manual(beat_name: str, url: str = SPACE_URL, no_cap: bool = False) -> Path | None:
478
+ """Manual mode: print beat cues with countdowns. If no_cap=False, also
479
+ starts/stops/exports via cap CLI. If no_cap=True, just prints cues β€” you
480
+ handle recording yourself (e.g. Cap desktop at higher quality)."""
481
  print(f"\n=== RECORD (manual): beat '{beat_name}' ===\n")
482
 
483
+ rec_id = None
484
+ cap_path = None
485
+
486
+ if not no_cap:
487
+ # 1) Start detached Cap recording
488
+ print(" starting Cap recording (detached)...")
489
+ screen_id = _get_screen_id()
490
+ started = _cap_json("record", "start", "--screen", screen_id, "--fps", CAP_FPS, "--detach", "--json")
491
+ if not started or "recordingId" not in started:
492
+ print(" βœ— failed to start recording")
493
+ return None
494
+ rec_id = started["recordingId"]
495
+ cap_path = started.get("path", "unknown")
496
+ print(f" βœ“ recording started (id={rec_id})")
497
+ else:
498
+ print(" 🎬 Start your Cap desktop recording NOW (high quality).")
499
+ time.sleep(2)
500
 
501
  # 2) Print beat cues β€” user drives the browser
502
  beats = MANUAL_BEATS.get(beat_name, MANUAL_BEATS["all"])
 
522
  time.sleep(1)
523
  print(f" βœ“ cut{' ' * 20}")
524
 
525
+ if no_cap:
526
+ print("\n πŸ›‘ Stop your Cap desktop recording now.")
527
+ print(f" πŸ“ Export from Cap desktop to: D:\\workspace\\recordings\\demo-{beat_name}-<ts>.mp4")
528
+ return None
529
+
530
  # 3) Stop recording
531
  print("\n stopping recording...")
532
  stopped = _cap_json("record", "stop", "--id", rec_id, "--json")
 
539
  ts = time.strftime("%Y%m%d-%H%M%S")
540
  out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
541
  print(f" exporting to {out_win} ...")
542
+ export_result = _cap("export", cap_path, "--output", out_win, "--quality", EXPORT_QUALITY, "--resolution", EXPORT_RES, "--json")
543
  if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
544
  print(f" βœ“ exported β†’ {out_win}")
545
  return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
 
574
  page = browser.new_page()
575
  page.set_viewport_size({"width": 1707, "height": 1067}) # native screen res
576
 
577
+ # Go fullscreen (F11) β€” hides taskbar + title bar for clean recording
578
+ page.keyboard.press("F11")
579
+ time.sleep(0.5)
580
+
581
  # Dismiss Chrome restore bubble + any initial popups
582
  time.sleep(1.0)
583
  _dismiss_popups(page)
 
629
  ts = time.strftime("%Y%m%d-%H%M%S")
630
  out_win = f"{EXPORT_DIR_WIN}\\demo-{beat_name}-{ts}.mp4"
631
  print(f" exporting to {out_win} ...")
632
+ export_result = _cap("export", cap_path, "--output", out_win, "--quality", EXPORT_QUALITY, "--resolution", EXPORT_RES, "--json")
633
  if "Completed" in export_result.stdout or "Progress" in export_result.stdout:
634
  print(f" βœ“ exported β†’ {out_win}")
635
  return Path(EXPORT_DIR_WSL) / f"demo-{beat_name}-{ts}.mp4"
 
648
  )
649
  ap.add_argument("--beat", default="all", choices=sorted(MANUAL_BEATS),
650
  help="which beat(s) to record (default: all)")
651
+ ap.add_argument("--mode", default="manual", choices=["manual", "auto", "cues"],
652
+ help="manual=cap CLI + cues; auto=Playwright; cues=just print beat cues (you record with Cap desktop)")
653
  ap.add_argument("--preflight-only", action="store_true",
654
  help="run preflight checks and exit")
655
  ap.add_argument("--slowmo", type=int, default=SLOWMO_DEFAULT,
 
671
 
672
  if args.mode == "auto":
673
  result = record_auto(args.beat, args.slowmo, args.pause, url)
674
+ elif args.mode == "cues":
675
+ result = record_manual(args.beat, url, no_cap=True)
676
  else:
677
  result = record_manual(args.beat, url)
678