fffiloni commited on
Commit
67265f9
·
verified ·
1 Parent(s): 90bbcf7

Upload 10 files

Browse files
Files changed (3) hide show
  1. src/bucket.py +49 -6
  2. src/progress.py +66 -12
  3. src/worker_payload.py +679 -41
src/bucket.py CHANGED
@@ -380,11 +380,16 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
380
  prefix = prefix.rstrip("/") + "/"
381
  return any(path.startswith(prefix) for path in file_paths)
382
 
383
- raw_present = has_prefix("traces/raw") or _has_glob(f"{paths.root}/traces/raw/**/*", token=token) or _has_glob(f"{paths.root}/traces/raw/*", token=token)
384
- redacted_present = has_prefix("traces/redacted") or _has_glob(f"{paths.root}/traces/redacted/**/*", token=token) or _has_glob(f"{paths.root}/traces/redacted/*", token=token)
 
 
385
  smoke_present = bool(bundle.get("generation_smoke")) or "tests/generation_smoke.json" in file_paths or _path_exists(f"{paths.root}/tests/generation_smoke.json", token=token)
386
  report_present = bool(bundle.get("report")) or _path_exists(paths.report, token=token)
387
  blockers_present = bool(bundle.get("technical_blockers")) or "generated/TECHNICAL_BLOCKERS.json" in file_paths or _path_exists(f"{paths.root}/generated/TECHNICAL_BLOCKERS.json", token=token) or _path_exists(f"{paths.root}/TECHNICAL_BLOCKERS.json", token=token)
 
 
 
388
  status = str((bundle.get("summary") or {}).get("status") or (bundle.get("state") or {}).get("status") or "").lower()
389
  blockers_relevant = blockers_present or any(marker in status for marker in ("manual", "blocker", "failed", "error"))
390
 
@@ -392,19 +397,19 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
392
  {
393
  "id": "pi_raw_trace",
394
  "label": "Pi RAW",
395
- "subtitle": "Full internal trace",
396
  "icon": "🧾",
397
  "present": raw_present,
398
- "url": _bucket_tree_url(bucket_source, run_id, "traces/raw"),
399
  "sensitivity": "raw",
400
  },
401
  {
402
  "id": "pi_redacted_trace",
403
  "label": "Pi redacted",
404
- "subtitle": "Safe to share",
405
  "icon": "🛡️",
406
  "present": redacted_present,
407
- "url": _bucket_tree_url(bucket_source, run_id, "traces/redacted"),
408
  "sensitivity": "safe",
409
  },
410
  {
@@ -424,6 +429,42 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
424
  "url": _bucket_file_url(bucket_source, run_id, "tests/generation_smoke.json"),
425
  },
426
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  if blockers_relevant:
428
  docs.append(
429
  {
@@ -576,6 +617,8 @@ def read_run_bundle(run_id: str, *, bucket_source: str, token: str | None = None
576
  "pi_model_resolution": _safe_read_json(f"{paths.root}/pi_model_resolution.json", token=token) or (read_json(paths.state, token=token) or {}).get("pi_model_resolution") or {},
577
  "space_runtime": _safe_read_json(f"{paths.root}/space_runtime.json", token=token),
578
  "api_schema": _safe_read_json(f"{paths.root}/tests/api_schema.json", token=token) or _safe_read_json(f"{paths.root}/tests/generation_api_schema.json", token=token),
 
 
579
  "files": _list_run_files(run_id, bucket_source=bucket_source, token=token) if include_heavy else [],
580
  }
581
  bundle["run_documents"] = _run_document_links(run_id, bucket_source=bucket_source, bundle=bundle, token=token)
 
380
  prefix = prefix.rstrip("/") + "/"
381
  return any(path.startswith(prefix) for path in file_paths)
382
 
383
+ raw_agent_trace_present = "traces/raw/agent_trace.jsonl" in file_paths or _path_exists(f"{paths.root}/traces/raw/agent_trace.jsonl", token=token)
384
+ redacted_agent_trace_present = "traces/redacted/agent_trace.jsonl" in file_paths or _path_exists(f"{paths.root}/traces/redacted/agent_trace.jsonl", token=token)
385
+ raw_present = raw_agent_trace_present or has_prefix("traces/raw") or _has_glob(f"{paths.root}/traces/raw/**/*", token=token) or _has_glob(f"{paths.root}/traces/raw/*", token=token)
386
+ redacted_present = redacted_agent_trace_present or has_prefix("traces/redacted") or _has_glob(f"{paths.root}/traces/redacted/**/*", token=token) or _has_glob(f"{paths.root}/traces/redacted/*", token=token)
387
  smoke_present = bool(bundle.get("generation_smoke")) or "tests/generation_smoke.json" in file_paths or _path_exists(f"{paths.root}/tests/generation_smoke.json", token=token)
388
  report_present = bool(bundle.get("report")) or _path_exists(paths.report, token=token)
389
  blockers_present = bool(bundle.get("technical_blockers")) or "generated/TECHNICAL_BLOCKERS.json" in file_paths or _path_exists(f"{paths.root}/generated/TECHNICAL_BLOCKERS.json", token=token) or _path_exists(f"{paths.root}/TECHNICAL_BLOCKERS.json", token=token)
390
+ repair_decision_present = "repair/REPAIR_DECISION.json" in file_paths or _path_exists(f"{paths.root}/repair/REPAIR_DECISION.json", token=token)
391
+ blockage_present = "repair/BLOCKAGE.json" in file_paths or _path_exists(f"{paths.root}/repair/BLOCKAGE.json", token=token)
392
+ repair_present = any(path.startswith("repair/") for path in file_paths) or _path_exists(f"{paths.root}/repair/REPAIR_SUMMARY.md", token=token)
393
  status = str((bundle.get("summary") or {}).get("status") or (bundle.get("state") or {}).get("status") or "").lower()
394
  blockers_relevant = blockers_present or any(marker in status for marker in ("manual", "blocker", "failed", "error"))
395
 
 
397
  {
398
  "id": "pi_raw_trace",
399
  "label": "Pi RAW",
400
+ "subtitle": "Unified build/diagnosis/repair trace",
401
  "icon": "🧾",
402
  "present": raw_present,
403
+ "url": _bucket_file_url(bucket_source, run_id, "traces/raw/agent_trace.jsonl") if raw_agent_trace_present else _bucket_tree_url(bucket_source, run_id, "traces/raw"),
404
  "sensitivity": "raw",
405
  },
406
  {
407
  "id": "pi_redacted_trace",
408
  "label": "Pi redacted",
409
+ "subtitle": "Unified safe agent trace",
410
  "icon": "🛡️",
411
  "present": redacted_present,
412
+ "url": _bucket_file_url(bucket_source, run_id, "traces/redacted/agent_trace.jsonl") if redacted_agent_trace_present else _bucket_tree_url(bucket_source, run_id, "traces/redacted"),
413
  "sensitivity": "safe",
414
  },
415
  {
 
429
  "url": _bucket_file_url(bucket_source, run_id, "tests/generation_smoke.json"),
430
  },
431
  ]
432
+ if repair_decision_present:
433
+ docs.append(
434
+ {
435
+ "id": "repair_decision",
436
+ "label": "Decision",
437
+ "subtitle": "Pi diagnosis action",
438
+ "icon": "◇",
439
+ "present": repair_decision_present,
440
+ "url": _bucket_file_url(bucket_source, run_id, "repair/REPAIR_DECISION.json"),
441
+ "tone": "warn" if "failed" in status or "error" in status else "neutral",
442
+ }
443
+ )
444
+ if repair_present:
445
+ docs.append(
446
+ {
447
+ "id": "repair",
448
+ "label": "Repair",
449
+ "subtitle": "Brief, plan and summary",
450
+ "icon": "✦",
451
+ "present": repair_present,
452
+ "url": _bucket_tree_url(bucket_source, run_id, "repair"),
453
+ "tone": "warn" if "failed" in status or "error" in status else "neutral",
454
+ }
455
+ )
456
+ if blockage_present:
457
+ docs.append(
458
+ {
459
+ "id": "blockage",
460
+ "label": "Blockage",
461
+ "subtitle": "Terminal recovery reason",
462
+ "icon": "!",
463
+ "present": blockage_present,
464
+ "url": _bucket_file_url(bucket_source, run_id, "repair/BLOCKAGE.json"),
465
+ "tone": "warn",
466
+ }
467
+ )
468
  if blockers_relevant:
469
  docs.append(
470
  {
 
617
  "pi_model_resolution": _safe_read_json(f"{paths.root}/pi_model_resolution.json", token=token) or (read_json(paths.state, token=token) or {}).get("pi_model_resolution") or {},
618
  "space_runtime": _safe_read_json(f"{paths.root}/space_runtime.json", token=token),
619
  "api_schema": _safe_read_json(f"{paths.root}/tests/api_schema.json", token=token) or _safe_read_json(f"{paths.root}/tests/generation_api_schema.json", token=token),
620
+ "repair_decision": _safe_read_json(f"{paths.root}/repair/REPAIR_DECISION.json", token=token),
621
+ "blockage": _safe_read_json(f"{paths.root}/repair/BLOCKAGE.json", token=token),
622
  "files": _list_run_files(run_id, bucket_source=bucket_source, token=token) if include_heavy else [],
623
  }
624
  bundle["run_documents"] = _run_document_links(run_id, bucket_source=bucket_source, bundle=bundle, token=token)
src/progress.py CHANGED
@@ -15,13 +15,28 @@ STEP_ORDER = [
15
  "pi_install",
16
  "pi_config",
17
  "pi_run",
 
18
  "pi_verification",
19
  "metadata_sanitize",
20
  "requirements_sanitize",
21
  "hardware_strategy",
22
  "create_space_hardware",
23
  "create_space",
 
 
 
 
 
 
24
  "repair",
 
 
 
 
 
 
 
 
25
  "upload_files",
26
  "space_runtime",
27
  "space_logs",
@@ -44,13 +59,28 @@ STEP_LABELS = {
44
  "pi_install": "Pi install",
45
  "pi_config": "Pi config",
46
  "pi_run": "Pi run",
 
47
  "pi_verification": "Pi verification",
48
  "metadata_sanitize": "Metadata sanitize",
49
  "requirements_sanitize": "Requirements sanitize",
50
  "hardware_strategy": "Hardware strategy",
51
  "create_space_hardware": "Create with hardware",
52
  "create_space": "Create Space",
53
- "repair": "Repair pass",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  "upload_files": "Upload files",
55
  "space_runtime": "Space runtime",
56
  "space_logs": "Space logs",
@@ -70,9 +100,12 @@ STEP_ALIASES = {
70
  "hardware_fallback": "hardware_strategy",
71
  "hardware": "hardware_strategy",
72
  "build": "space_runtime",
73
- "diagnose": "repair",
74
- "diagnosis": "repair",
75
- "patch": "repair",
 
 
 
76
  }
77
 
78
  DONE_STATUSES = {
@@ -196,18 +229,21 @@ def progress_from_events(events: list[dict[str, Any]] | None, *, state: dict[str
196
  terminal_status = "cancelled"
197
  elif status_from_state in DONE_STATUSES:
198
  terminal_status = status_from_state
199
- elif status_from_state in FAILED_STATUSES and (not events or current_status in FAILED_STATUSES or current_step == "failure"):
200
  terminal_status = "failed"
 
 
 
 
201
  elif job_stage in CANCELLED_STATUSES:
202
  terminal_status = "cancelled"
203
  elif job_stage in FAILED_STATUSES:
204
- # HF Job failed is authoritative only when the worker did not emit a
205
- # later waiting/running validation/manual state. This avoids showing a
206
- # recovered run as failed, but still turns genuinely stopped jobs red.
207
- if not events or current_status in FAILED_STATUSES or current_step == "failure":
208
- terminal_status = "failed"
209
- elif current_status not in RUNNING_STATUSES:
210
- terminal_status = "failed"
211
 
212
  if terminal_status:
213
  overall_status = terminal_status
@@ -230,6 +266,24 @@ def progress_from_events(events: list[dict[str, Any]] | None, *, state: dict[str
230
  for step in STEP_ORDER:
231
  if step != "failure":
232
  step_status[step] = "done"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  else:
234
  for idx, step in enumerate(STEP_ORDER):
235
  if idx < furthest_index:
 
15
  "pi_install",
16
  "pi_config",
17
  "pi_run",
18
+ "pi_model_resolution",
19
  "pi_verification",
20
  "metadata_sanitize",
21
  "requirements_sanitize",
22
  "hardware_strategy",
23
  "create_space_hardware",
24
  "create_space",
25
+ "failure_detected",
26
+ "failure_diagnosis",
27
+ "pi_diagnosis",
28
+ "repair_decision",
29
+ "wait_for_logs",
30
+ "factory_rebuild",
31
  "repair",
32
+ "repair_diagnosis",
33
+ "repair_brief",
34
+ "repair_plan",
35
+ "repair_patch",
36
+ "repair_upload",
37
+ "repair_validation",
38
+ "technical_blocker",
39
+ "manual_hardware_required",
40
  "upload_files",
41
  "space_runtime",
42
  "space_logs",
 
59
  "pi_install": "Pi install",
60
  "pi_config": "Pi config",
61
  "pi_run": "Pi run",
62
+ "pi_model_resolution": "Pi model",
63
  "pi_verification": "Pi verification",
64
  "metadata_sanitize": "Metadata sanitize",
65
  "requirements_sanitize": "Requirements sanitize",
66
  "hardware_strategy": "Hardware strategy",
67
  "create_space_hardware": "Create with hardware",
68
  "create_space": "Create Space",
69
+ "failure_detected": "Failure detected",
70
+ "failure_diagnosis": "Failure diagnosis",
71
+ "pi_diagnosis": "Pi diagnosis",
72
+ "repair_decision": "Repair decision",
73
+ "wait_for_logs": "Wait for logs",
74
+ "factory_rebuild": "Factory rebuild",
75
+ "repair": "Repair start",
76
+ "repair_diagnosis": "Patch allowed",
77
+ "repair_brief": "Repair brief",
78
+ "repair_plan": "Repair plan",
79
+ "repair_patch": "Repair patch",
80
+ "repair_upload": "Repair upload",
81
+ "repair_validation": "Repair validation",
82
+ "technical_blocker": "Technical blocker",
83
+ "manual_hardware_required": "Manual hardware",
84
  "upload_files": "Upload files",
85
  "space_runtime": "Space runtime",
86
  "space_logs": "Space logs",
 
100
  "hardware_fallback": "hardware_strategy",
101
  "hardware": "hardware_strategy",
102
  "build": "space_runtime",
103
+ "diagnose": "failure_diagnosis",
104
+ "diagnosis": "failure_diagnosis",
105
+ "pi_decision": "repair_decision",
106
+ "decision": "repair_decision",
107
+ "patch": "repair_patch",
108
+ "repair_failed": "repair_validation",
109
  }
110
 
111
  DONE_STATUSES = {
 
229
  terminal_status = "cancelled"
230
  elif status_from_state in DONE_STATUSES:
231
  terminal_status = status_from_state
232
+ elif status_from_state in FAILED_STATUSES:
233
  terminal_status = "failed"
234
+ if current_status not in FAILED_STATUSES and current_step != "failure":
235
+ current_step = "failure"
236
+ current_status = "failed"
237
+ furthest_index = STEP_ORDER.index("failure")
238
  elif job_stage in CANCELLED_STATUSES:
239
  terminal_status = "cancelled"
240
  elif job_stage in FAILED_STATUSES:
241
+ # A terminal HF Job failure must produce an explicit red timeline point.
242
+ terminal_status = "failed"
243
+ if current_status not in FAILED_STATUSES and current_step != "failure":
244
+ current_step = "failure"
245
+ current_status = "failed"
246
+ furthest_index = STEP_ORDER.index("failure")
 
247
 
248
  if terminal_status:
249
  overall_status = terminal_status
 
266
  for step in STEP_ORDER:
267
  if step != "failure":
268
  step_status[step] = "done"
269
+ elif overall_status in FAILED_STATUSES:
270
+ failure_idx = STEP_ORDER.index("failure")
271
+ if current_step in STEP_ORDER and current_step != "failure":
272
+ # Preserve the failed worker step when it is known; otherwise expose
273
+ # the explicit Failure stage so the UI always has a red dot.
274
+ failed_idx = STEP_ORDER.index(current_step)
275
+ else:
276
+ failed_idx = failure_idx
277
+ current_step = "failure"
278
+ furthest_index = max(furthest_index, failed_idx)
279
+ for idx, step in enumerate(STEP_ORDER):
280
+ if idx < failed_idx:
281
+ step_status[step] = "done"
282
+ elif idx == failed_idx:
283
+ step_status[step] = "failed"
284
+ else:
285
+ step_status[step] = "pending"
286
+ step_status["failure"] = "failed"
287
  else:
288
  for idx, step in enumerate(STEP_ORDER):
289
  if idx < furthest_index:
src/worker_payload.py CHANGED
@@ -60,6 +60,66 @@ def redact_text(text: str | None) -> str:
60
  return value
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def safe_details(details: dict | None) -> dict:
64
  if not details:
65
  return {}
@@ -170,6 +230,7 @@ def run_cmd_streaming(
170
  events_path: Path | None = None,
171
  run_dir: Path | None = None,
172
  step: str = "pi_run",
 
173
  ):
174
  """Run a command while incrementally writing stdout to the bucket.
175
 
@@ -178,12 +239,15 @@ def run_cmd_streaming(
178
  JSONL traces into traces/redacted while the process is still alive.
179
  """
180
  started = time.monotonic()
 
181
  last_event = 0.0
182
  last_trace_sync = 0.0
183
  lines: list[str] = []
184
  if live_log_path:
185
  live_log_path.parent.mkdir(parents=True, exist_ok=True)
186
  live_log_path.write_text("", encoding="utf-8")
 
 
187
  proc = subprocess.Popen(
188
  cmd,
189
  cwd=str(cwd) if cwd else None,
@@ -214,7 +278,10 @@ def run_cmd_streaming(
214
  f.write(clean + "\n")
215
  now_m = time.monotonic()
216
  if events_path and now_m - last_event > 8:
217
- append_event(events_path, step, "running", "Pi is still working", {"output_tail": "\n".join(lines[-20:])[-2000:]})
 
 
 
218
  last_event = now_m
219
  if run_dir and now_m - last_trace_sync > 15:
220
  sync_pi_traces(run_dir, emit_event=False)
@@ -235,7 +302,10 @@ def run_cmd_streaming(
235
  finally:
236
  if run_dir:
237
  sync_pi_traces(run_dir, emit_event=False)
238
- return int(proc.returncode or 0), redact_text("\n".join(lines))
 
 
 
239
 
240
 
241
  def install_python_deps(events_path: Path):
@@ -383,7 +453,8 @@ def emit_pi_model_resolution(events_path: Path, resolution: dict):
383
 
384
  def collect_pi_traces(run_dir: Path, events_path: Path):
385
  count = sync_pi_traces(run_dir, emit_event=False)
386
- append_event(events_path, "traces", "success", "Collected Pi traces", {"count": count})
 
387
  return count
388
 
389
 
@@ -1079,66 +1150,633 @@ def normalize_requirements_for_modern_hub(workspace: Path, events_path: Path):
1079
  append_event(events_path, "requirements_sanitize", "success", "Normalized huggingface_hub requirement for modern dependency resolution", {"huggingface_hub": hub_line})
1080
 
1081
 
1082
- def repair_workspace_with_pi(workspace: Path, run_dir: Path, events_path: Path, pi_model: str, target_space_id: str, model_id: str, failure_reason: str):
1083
- """Ask Pi for one minimal build/runtime repair pass based on collected logs."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1084
  logs_dir = run_dir / "logs"
1085
  build_log = (logs_dir / "space_logs_build.txt").read_text(encoding="utf-8", errors="ignore") if (logs_dir / "space_logs_build.txt").exists() else ""
1086
  runtime_log = (logs_dir / "space_logs_runtime.txt").read_text(encoding="utf-8", errors="ignore") if (logs_dir / "space_logs_runtime.txt").exists() else ""
1087
  repair_dir = run_dir / "repair"
1088
- before_dir = repair_dir / "before"
1089
- after_dir = repair_dir / "after"
1090
- if before_dir.exists():
1091
- shutil.rmtree(before_dir)
1092
- shutil.copytree(workspace, before_dir, ignore=shutil.ignore_patterns(".git", "node_modules", "__pycache__", "*.pyc"))
1093
- goal = f"""You are Pi repairing a Hugging Face Space generated by Agentic Space Factory.
 
 
 
 
 
1094
 
1095
- MODEL_ID: {model_id}
1096
- TARGET_SPACE_ID: {target_space_id}
 
 
1097
 
1098
- The first build/runtime validation failed.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1099
 
1100
- Failure summary:
1101
- {failure_reason[:4000]}
 
 
 
 
1102
 
1103
- Build log tail:
 
 
 
 
 
 
 
 
 
 
 
1104
  ```text
1105
  {build_log[-12000:]}
1106
  ```
1107
 
1108
- Runtime log tail:
1109
  ```text
1110
  {runtime_log[-12000:]}
1111
  ```
1112
 
1113
- Repair contract:
1114
- - Make the smallest patch possible.
1115
- - Prefer fixing dependency resolver conflicts, missing imports, invalid metadata, Gradio endpoint bugs, and import-order issues.
1116
- - Do not replace real inference with a placeholder unless TECHNICAL_BLOCKERS.json clearly explains why full inference is impossible.
1117
- - Preserve a cheap health endpoint with api_name="health".
1118
- - Keep README frontmatter valid, short_description <= 60 chars.
1119
- - Do not pin huggingface_hub below 1.0. For modern generated Spaces use huggingface_hub>=0.34.0,<2.0.0. If transformers>=5 is present, use huggingface_hub>=1.5.0,<2.0.0.
1120
- - Do not delete the app. Do not publish anything. Work only in the current workspace.
1121
 
1122
- Deliverables:
1123
- - patched app.py / requirements.txt / README.md as needed
1124
- - REPAIR_SUMMARY.md explaining the patch
1125
- - keep or update INFERENCE_CONTRACT.json if the inference contract changed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1126
  """
1127
  (workspace / "REPAIR_GOAL.md").write_text(goal, encoding="utf-8")
1128
- append_event(events_path, "repair", "started", "Running Pi repair pass using build/runtime logs", {"model": pi_model})
 
 
1129
  code, out = run_cmd(["pi", "-p", goal], cwd=workspace, timeout=1500)
1130
  logs_dir.mkdir(parents=True, exist_ok=True)
1131
  (logs_dir / "pi_repair_output.txt").write_text(out, encoding="utf-8")
 
1132
  if code != 0:
1133
- append_event(events_path, "repair", "failed", "Pi repair returned a non-zero exit code", {"returncode": code, "output_tail": out[-3000:]})
1134
  return False
 
 
 
 
 
 
 
 
 
 
 
 
1135
  normalize_requirements_for_modern_hub(workspace, events_path)
 
 
 
 
 
 
1136
  if after_dir.exists():
1137
  shutil.rmtree(after_dir)
1138
  shutil.copytree(workspace, after_dir, ignore=shutil.ignore_patterns(".git", "node_modules", "__pycache__", "*.pyc"))
1139
- append_event(events_path, "repair", "success", "Pi repair pass completed", {"output_tail": out[-3000:]})
1140
  return True
1141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1142
  def upload_workspace(api, workspace: Path, target_space_id: str, token: str, run_dir: Path, events_path: Path):
1143
  sanitize_readme_metadata(workspace, events_path)
1144
  normalize_requirements_for_modern_hub(workspace, events_path)
@@ -1155,7 +1793,7 @@ def upload_workspace(api, workspace: Path, target_space_id: str, token: str, run
1155
  repo_id=target_space_id,
1156
  repo_type="space",
1157
  token=token,
1158
- ignore_patterns=[".git/*", "node_modules/*", "__pycache__/*", "*.pyc", "GOAL.md"],
1159
  )
1160
  uploaded_files = sorted(str(p.relative_to(workspace)) for p in workspace.rglob("*") if p.is_file() and "node_modules" not in p.parts and "__pycache__" not in p.parts)
1161
  append_event(events_path, "upload_files", "success", "Uploaded generated workspace folder", {"file_count": len(uploaded_files), "files_sample": uploaded_files[:50]})
@@ -1323,6 +1961,7 @@ def main():
1323
  events_path=events_path,
1324
  run_dir=run_dir,
1325
  step="pi_run",
 
1326
  )
1327
  (run_dir / "logs" / "pi_output.txt").write_text(pi_out, encoding="utf-8")
1328
  if code != 0:
@@ -1373,13 +2012,12 @@ def main():
1373
  try:
1374
  validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
1375
  except Exception as validation_error:
1376
- append_event(events_path, "repair", "started", "Initial live validation failed; attempting one repair pass", {"error": str(validation_error)[:2000]})
1377
- collect_space_logs(target_space_id, token, run_dir, events_path)
1378
- repaired = repair_workspace_with_pi(workspace, run_dir, events_path, pi_model, target_space_id, model_id, str(validation_error))
1379
- if not repaired:
1380
- raise
1381
- upload_workspace(api, workspace, target_space_id, token, run_dir, events_path)
1382
- validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
1383
  generation_smoke = None
1384
  if implementation_mode in {"full-inference-gated", "full-inference-attempt"}:
1385
  try:
 
60
  return value
61
 
62
 
63
+
64
+
65
+ def _agent_trace_json_safe(payload):
66
+ try:
67
+ return json.loads(redact_text(json.dumps(payload, ensure_ascii=False)))
68
+ except Exception:
69
+ return {"value": redact_text(str(payload))[:8000]}
70
+
71
+
72
+ def write_agent_trace_record(
73
+ run_dir: Path,
74
+ *,
75
+ phase: str,
76
+ event: str,
77
+ status: str = "info",
78
+ message: str = "",
79
+ data: dict | None = None,
80
+ text: str | None = None,
81
+ artifacts: list[str] | None = None,
82
+ ):
83
+ """Append one run-level agent trace record to RAW and redacted journals.
84
+
85
+ Pi can be invoked several times during a run: initial build, blockage
86
+ diagnosis, and optional repair patch. The individual Pi session files and
87
+ stdout logs remain useful, but this journal is the canonical narrative tying
88
+ those phases together into one auditable agent run.
89
+ """
90
+ try:
91
+ payload = {
92
+ "ts": now(),
93
+ "phase": phase,
94
+ "event": event,
95
+ "status": status,
96
+ "message": message,
97
+ "data": data or {},
98
+ "artifacts": artifacts or [],
99
+ }
100
+ if text is not None:
101
+ payload["text"] = text
102
+ raw_dir = run_dir / "traces" / "raw"
103
+ redacted_dir = run_dir / "traces" / "redacted"
104
+ raw_dir.mkdir(parents=True, exist_ok=True)
105
+ redacted_dir.mkdir(parents=True, exist_ok=True)
106
+ raw_line = json.dumps(payload, ensure_ascii=False)
107
+ redacted_payload = _agent_trace_json_safe(payload)
108
+ redacted_line = json.dumps(redacted_payload, ensure_ascii=False)
109
+ with (raw_dir / "agent_trace.jsonl").open("a", encoding="utf-8") as f:
110
+ f.write(raw_line + "\n")
111
+ with (redacted_dir / "agent_trace.jsonl").open("a", encoding="utf-8") as f:
112
+ f.write(redacted_line + "\n")
113
+ except Exception:
114
+ # Trace journaling must never break the build/repair path.
115
+ pass
116
+
117
+
118
+ def append_agent_trace_artifact(run_dir: Path, *, phase: str, event: str, artifact: str, text: str | None = None, status: str = "success", data: dict | None = None):
119
+ tail = None if text is None else text[-20000:]
120
+ write_agent_trace_record(run_dir, phase=phase, event=event, status=status, message=f"Agent phase wrote {artifact}", data=data or {}, text=tail, artifacts=[artifact])
121
+
122
+
123
  def safe_details(details: dict | None) -> dict:
124
  if not details:
125
  return {}
 
230
  events_path: Path | None = None,
231
  run_dir: Path | None = None,
232
  step: str = "pi_run",
233
+ trace_phase: str | None = None,
234
  ):
235
  """Run a command while incrementally writing stdout to the bucket.
236
 
 
239
  JSONL traces into traces/redacted while the process is still alive.
240
  """
241
  started = time.monotonic()
242
+ trace_phase = trace_phase or step
243
  last_event = 0.0
244
  last_trace_sync = 0.0
245
  lines: list[str] = []
246
  if live_log_path:
247
  live_log_path.parent.mkdir(parents=True, exist_ok=True)
248
  live_log_path.write_text("", encoding="utf-8")
249
+ if run_dir:
250
+ write_agent_trace_record(run_dir, phase=trace_phase, event="command_started", status="started", message="Pi command started", data={"step": step, "cwd": str(cwd) if cwd else ""})
251
  proc = subprocess.Popen(
252
  cmd,
253
  cwd=str(cwd) if cwd else None,
 
278
  f.write(clean + "\n")
279
  now_m = time.monotonic()
280
  if events_path and now_m - last_event > 8:
281
+ output_tail = "\n".join(lines[-20:])[-2000:]
282
+ append_event(events_path, step, "running", "Pi is still working", {"output_tail": output_tail})
283
+ if run_dir:
284
+ write_agent_trace_record(run_dir, phase=trace_phase, event="stdout_tail", status="running", message="Pi is still working", text=output_tail, data={"step": step})
285
  last_event = now_m
286
  if run_dir and now_m - last_trace_sync > 15:
287
  sync_pi_traces(run_dir, emit_event=False)
 
302
  finally:
303
  if run_dir:
304
  sync_pi_traces(run_dir, emit_event=False)
305
+ output = redact_text("\n".join(lines))
306
+ if run_dir:
307
+ write_agent_trace_record(run_dir, phase=trace_phase, event="command_finished", status="success" if int(proc.returncode or 0) == 0 else "failed", message="Pi command finished", text=output[-20000:], data={"step": step, "returncode": int(proc.returncode or 0)}, artifacts=[str(live_log_path.relative_to(run_dir)) if live_log_path and str(live_log_path).startswith(str(run_dir)) else ""])
308
+ return int(proc.returncode or 0), output
309
 
310
 
311
  def install_python_deps(events_path: Path):
 
453
 
454
  def collect_pi_traces(run_dir: Path, events_path: Path):
455
  count = sync_pi_traces(run_dir, emit_event=False)
456
+ write_agent_trace_record(run_dir, phase="initial_build", event="pi_sessions_collected", status="success", message="Synchronized Pi session traces into the run-level trace folder", data={"count": count}, artifacts=["traces/raw/agent_trace.jsonl", "traces/redacted/agent_trace.jsonl"])
457
+ append_event(events_path, "traces", "success", "Collected Pi traces", {"count": count, "agent_trace": "traces/redacted/agent_trace.jsonl"})
458
  return count
459
 
460
 
 
1150
  append_event(events_path, "requirements_sanitize", "success", "Normalized huggingface_hub requirement for modern dependency resolution", {"huggingface_hub": hub_line})
1151
 
1152
 
1153
+
1154
+ def useful_log_signals(text: str) -> list[str]:
1155
+ low = (text or "").lower()
1156
+ signals = []
1157
+ patterns = {
1158
+ "python_exception": ["traceback", "modulenotfounderror", "importerror", "runtimeerror", "valueerror", "typeerror"],
1159
+ "dependency_resolution": ["resolutionimpossible", "no matching distribution", "could not find a version", "pip subprocess", "metadata-generation-failed"],
1160
+ "gradio_api": ["api_name", "endpoint", "unexpected keyword", "too many arguments", "not enough arguments", "no api found"],
1161
+ "model_loading": ["from_pretrained", "model_index", "safetensors", "does not appear to have", "failed to load", "pipeline"],
1162
+ "hardware_memory": ["cuda out of memory", "outofmemoryerror", "oom", "not enough memory"],
1163
+ "auth_access": ["401", "403", "gated", "unauthorized", "forbidden", "repository not found"],
1164
+ "zerogpu": ["zerogpu", "spaces.gpu", "duration"],
1165
+ }
1166
+ for name, needles in patterns.items():
1167
+ if any(n in low for n in needles):
1168
+ signals.append(name)
1169
+ return signals
1170
+
1171
+
1172
+ def log_quality(build_log: str = "", runtime_log: str = "", failure_reason: str = "") -> str:
1173
+ combined = f"{build_log}\n{runtime_log}\n{failure_reason}".strip()
1174
+ low = combined.lower()
1175
+ if not combined or len(combined) < 80:
1176
+ return "empty"
1177
+ no_reason_markers = [
1178
+ "no logs", "logs are empty", "no reason", "unknown error", "runtime error", "build error", "error: none", "status=error",
1179
+ ]
1180
+ if any(m in low for m in no_reason_markers) and not useful_log_signals(combined):
1181
+ return "no_reason"
1182
+ if useful_log_signals(combined):
1183
+ return "useful"
1184
+ return "partial"
1185
+
1186
+
1187
+ def classify_repair_failure(failure_reason: str, build_log: str = "", runtime_log: str = "") -> dict:
1188
+ """Classify a failed live validation before asking Pi to choose an action.
1189
+
1190
+ This classifier is deliberately conservative. It gives Pi and the Factory
1191
+ evidence quality and a suggested class, but the diagnosis step must still
1192
+ make a bounded decision before any patch is allowed.
1193
+ """
1194
+ text = f"{failure_reason}\n{build_log[-6000:]}\n{runtime_log[-6000:]}".lower()
1195
+ quality = log_quality(build_log, runtime_log, failure_reason)
1196
+ phase = "unknown"
1197
+ if "build" in text or "pip" in text or "container" in text:
1198
+ phase = "space_build"
1199
+ if "runtime" in text or "health" in text or "client.predict" in text or "api" in text:
1200
+ phase = "space_runtime"
1201
+ if "generation" in text or "smoke" in text or "expected output" in text:
1202
+ phase = "api_validation"
1203
+
1204
+ checks = [
1205
+ ("cuda_oom", ["cuda out of memory", "outofmemoryerror", "oom", "not enough memory"], "Prefer hardware/manual action or memory reduction; do not fake inference."),
1206
+ ("dependency_error", ["resolutionimpossible", "could not find a version", "no matching distribution", "dependency conflict", "pip subprocess", "metadata-generation-failed"], "Fix requirements with compatible, modern pins."),
1207
+ ("import_error", ["modulenotfounderror", "importerror", "cannot import name"], "Fix imports or requirements without hiding the error."),
1208
+ ("gradio_api_mismatch", ["no api found", "api_name", "endpoint", "unexpected keyword", "too many arguments", "not enough arguments"], "Preserve the expected Gradio endpoint and align input/output schema."),
1209
+ ("model_loading_error", ["from_pretrained", "safetensors", "model_index", "pipeline", "failed to load", "does not appear to have"], "Re-read the model card usage and fix the pipeline loading path."),
1210
+ ("hf_auth_error", ["401", "403", "gated", "unauthorized", "forbidden", "repository not found"], "Do not bypass auth; report gated/private access if needed."),
1211
+ ("space_boot_timeout", ["timed out", "timeout", "space did not become ready", "sleeping"], "Inspect logs first; if logs are empty, wait/rebuild same code before patching."),
1212
+ ("wrong_output_type", ["wrong output", "expected output", "invalid output", "not an image", "not text"], "Return the expected output type from real inference."),
1213
+ ("zero_gpu_duration_error", ["duration", "spaces.gpu", "zerogpu", "zero gpu"], "Tune @spaces.GPU(duration=...) from observed smoke-test latency when available."),
1214
+ ]
1215
+ category = "unknown_runtime_error"
1216
+ recommendation = "Use the Space logs, Gradio API schema and smoke error to choose the smallest truthful action."
1217
+ for code, needles, rec in checks:
1218
+ if any(n in text for n in needles):
1219
+ category = code
1220
+ recommendation = rec
1221
+ break
1222
+ if quality in {"empty", "no_reason"} and category == "unknown_runtime_error":
1223
+ category = "hf_runtime_flake" if phase != "space_build" else "hf_build_flake"
1224
+ recommendation = "Logs are not actionable. Prefer wait/inspect or a same-code factory rebuild before any code patch."
1225
+ return {
1226
+ "category": category,
1227
+ "failure_phase": phase,
1228
+ "logs_quality": quality,
1229
+ "signals": useful_log_signals(text),
1230
+ "recommendation": recommendation,
1231
+ }
1232
+
1233
+
1234
+ def workspace_file_inventory(workspace: Path, max_files: int = 80) -> list[str]:
1235
+ files = []
1236
+ for path in sorted(workspace.rglob("*")):
1237
+ if not path.is_file():
1238
+ continue
1239
+ if any(part in {".git", "node_modules", "__pycache__"} for part in path.parts):
1240
+ continue
1241
+ try:
1242
+ files.append(str(path.relative_to(workspace)))
1243
+ except Exception:
1244
+ files.append(path.name)
1245
+ if len(files) >= max_files:
1246
+ break
1247
+ return files
1248
+
1249
+
1250
+ def write_incident_brief(workspace: Path, run_dir: Path, *, target_space_id: str, model_id: str, pi_model: str, failure_reason: str, build_log: str, runtime_log: str, classification: dict, implementation_mode: str, expected_output_type: str, iteration: int, budgets: dict | None = None) -> str:
1251
+ repair_dir = run_dir / "repair"
1252
+ repair_dir.mkdir(parents=True, exist_ok=True)
1253
+ inventory = workspace_file_inventory(workspace)
1254
+ budgets = budgets or {}
1255
+ brief = f"""# Agentic Space Factory incident brief
1256
+
1257
+ This is a blockage diagnosis pass for an existing generated Hugging Face Space.
1258
+ Pi must use the HF Spaces gist method: read logs first, act once, choose the cheapest useful iteration rung, and verify on the live Space.
1259
+
1260
+ ## Target
1261
+ - Model ID: `{model_id}`
1262
+ - Target Space: `{target_space_id}`
1263
+ - Pi model: `{pi_model}`
1264
+ - Implementation mode: `{implementation_mode}`
1265
+ - Expected output type: `{expected_output_type}`
1266
+ - Diagnosis iteration: `{iteration}`
1267
+
1268
+ ## Current classifier hints
1269
+ ```json
1270
+ {json.dumps(classification, indent=2, ensure_ascii=False)}
1271
+ ```
1272
+
1273
+ ## Remaining action budget
1274
+ ```json
1275
+ {json.dumps(budgets, indent=2, ensure_ascii=False)}
1276
+ ```
1277
+
1278
+ ## Observed failure
1279
+ ```text
1280
+ {failure_reason[:5000]}
1281
+ ```
1282
+
1283
+ ## Build log tail
1284
+ ```text
1285
+ {build_log[-12000:]}
1286
+ ```
1287
+
1288
+ ## Runtime log tail
1289
+ ```text
1290
+ {runtime_log[-12000:]}
1291
+ ```
1292
+
1293
+ ## Workspace files
1294
+ ```text
1295
+ {chr(10).join(inventory)}
1296
+ ```
1297
+
1298
+ ## Allowed decisions
1299
+ You must choose exactly one action:
1300
+ - `wait_for_logs`: logs are empty/late; wait and collect again.
1301
+ - `inspect_more_logs`: enough uncertainty remains; collect logs/API status again without changing code.
1302
+ - `factory_rebuild_same_code`: likely HF build/runtime flake; re-upload the same workspace to force a rebuild, then revalidate.
1303
+ - `patch_code`: logs are actionable and point to a code/dependency/API/model-loading issue.
1304
+ - `request_manual_hardware`: issue is hardware/quota/restricted GPU/memory and should not be patched away.
1305
+ - `declare_technical_blocker`: no safe automated action remains.
1306
+
1307
+ ## Hard rules
1308
+ - Do not choose `patch_code` when logs are empty or no-reason unless you cite concrete evidence from files/API schema.
1309
+ - Do not fake inference or replace model output with placeholders.
1310
+ - Do not patch hardware/quota/authorization failures as code bugs.
1311
+ - If logs are actionable, identify the first real error, not only the last line.
1312
+ - If using `factory_rebuild_same_code`, do not change files.
1313
+
1314
+ ## Required output
1315
+ Write `REPAIR_DECISION.json` in the workspace root with this schema:
1316
+ ```json
1317
+ {{
1318
+ "action": "wait_for_logs|inspect_more_logs|factory_rebuild_same_code|patch_code|request_manual_hardware|declare_technical_blocker",
1319
+ "confidence": "low|medium|high",
1320
+ "reason": "short explanation",
1321
+ "evidence": ["specific evidence"],
1322
+ "patch_allowed": false,
1323
+ "requires_manual_hardware": false
1324
+ }}
1325
+ ```
1326
+ """
1327
+ (repair_dir / "INCIDENT_BRIEF.md").write_text(brief, encoding="utf-8")
1328
+ (workspace / "INCIDENT_BRIEF.md").write_text(brief, encoding="utf-8")
1329
+ write_json(repair_dir / "classification.json", classification)
1330
+ return brief
1331
+
1332
+
1333
+ def extract_json_object(text: str) -> dict:
1334
+ if not text:
1335
+ return {}
1336
+ candidates = []
1337
+ fenced = re.findall(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.S)
1338
+ candidates.extend(fenced)
1339
+ first = text.find("{")
1340
+ last = text.rfind("}")
1341
+ if first != -1 and last != -1 and last > first:
1342
+ candidates.append(text[first:last + 1])
1343
+ for candidate in candidates:
1344
+ try:
1345
+ value = json.loads(candidate)
1346
+ if isinstance(value, dict):
1347
+ return value
1348
+ except Exception:
1349
+ continue
1350
+ return {}
1351
+
1352
+
1353
+ def fallback_decision_from_classifier(classification: dict, budgets: dict | None = None) -> dict:
1354
+ budgets = budgets or {}
1355
+ category = classification.get("category") or "unknown_runtime_error"
1356
+ quality = classification.get("logs_quality") or "partial"
1357
+ if category in {"hf_auth_error"}:
1358
+ action = "declare_technical_blocker"
1359
+ elif category == "cuda_oom":
1360
+ action = "request_manual_hardware"
1361
+ elif quality in {"empty", "no_reason"}:
1362
+ if budgets.get("wait_for_logs", 0) > 0:
1363
+ action = "wait_for_logs"
1364
+ elif budgets.get("factory_rebuild_same_code", 0) > 0:
1365
+ action = "factory_rebuild_same_code"
1366
+ else:
1367
+ action = "declare_technical_blocker"
1368
+ elif category in {"dependency_error", "import_error", "gradio_api_mismatch", "model_loading_error", "wrong_output_type", "zero_gpu_duration_error", "space_boot_timeout"}:
1369
+ action = "patch_code" if budgets.get("patch_code", 0) > 0 else "declare_technical_blocker"
1370
+ elif budgets.get("factory_rebuild_same_code", 0) > 0:
1371
+ action = "factory_rebuild_same_code"
1372
+ elif budgets.get("patch_code", 0) > 0 and quality == "useful":
1373
+ action = "patch_code"
1374
+ else:
1375
+ action = "declare_technical_blocker"
1376
+ return {
1377
+ "action": action,
1378
+ "confidence": "low",
1379
+ "reason": f"Factory fallback decision from classifier category={category}, logs_quality={quality}.",
1380
+ "evidence": classification.get("signals") or [],
1381
+ "patch_allowed": action == "patch_code",
1382
+ "requires_manual_hardware": action == "request_manual_hardware",
1383
+ "source": "factory_fallback",
1384
+ }
1385
+
1386
+
1387
+ def normalize_repair_decision(decision: dict, classification: dict, budgets: dict | None = None) -> dict:
1388
+ budgets = budgets or {}
1389
+ allowed = {"wait_for_logs", "inspect_more_logs", "factory_rebuild_same_code", "patch_code", "request_manual_hardware", "declare_technical_blocker"}
1390
+ action = str(decision.get("action") or "").strip().lower().replace("-", "_")
1391
+ if action not in allowed:
1392
+ decision = fallback_decision_from_classifier(classification, budgets)
1393
+ action = decision["action"]
1394
+ quality = classification.get("logs_quality") or "partial"
1395
+ category = classification.get("category") or "unknown_runtime_error"
1396
+ overrides = []
1397
+ if action == "patch_code" and quality in {"empty", "no_reason"} and category in {"hf_runtime_flake", "hf_build_flake", "unknown_runtime_error"}:
1398
+ overrides.append("patch_code_blocked_without_actionable_logs")
1399
+ if budgets.get("wait_for_logs", 0) > 0:
1400
+ action = "wait_for_logs"
1401
+ elif budgets.get("factory_rebuild_same_code", 0) > 0:
1402
+ action = "factory_rebuild_same_code"
1403
+ else:
1404
+ action = "declare_technical_blocker"
1405
+ if action in budgets and budgets.get(action, 0) <= 0:
1406
+ overrides.append(f"budget_exhausted:{action}")
1407
+ action = "declare_technical_blocker"
1408
+ if action == "patch_code" and category in {"hf_auth_error"}:
1409
+ overrides.append("auth_failure_not_patchable")
1410
+ action = "declare_technical_blocker"
1411
+ if action == "patch_code" and category == "cuda_oom":
1412
+ # The agent may sometimes reduce memory pressure, but default to manual
1413
+ # hardware unless the logs are useful and it explicitly gave evidence.
1414
+ evidence = " ".join(str(x).lower() for x in decision.get("evidence") or [])
1415
+ if "dtype" not in evidence and "offload" not in evidence and "steps" not in evidence:
1416
+ overrides.append("oom_needs_hardware_or_explicit_memory_plan")
1417
+ action = "request_manual_hardware"
1418
+ normalized = {
1419
+ "action": action,
1420
+ "confidence": str(decision.get("confidence") or "low"),
1421
+ "reason": str(decision.get("reason") or "No reason provided.")[:2000],
1422
+ "evidence": list(decision.get("evidence") or [])[:20],
1423
+ "patch_allowed": action == "patch_code",
1424
+ "requires_manual_hardware": action == "request_manual_hardware",
1425
+ "factory_overrides": overrides,
1426
+ "classification": classification,
1427
+ }
1428
+ return normalized
1429
+
1430
+
1431
+ def diagnose_failure_with_pi(workspace: Path, run_dir: Path, events_path: Path, pi_model: str, target_space_id: str, model_id: str, failure_reason: str, implementation_mode: str, expected_output_type: str, iteration: int, budgets: dict) -> dict:
1432
  logs_dir = run_dir / "logs"
1433
  build_log = (logs_dir / "space_logs_build.txt").read_text(encoding="utf-8", errors="ignore") if (logs_dir / "space_logs_build.txt").exists() else ""
1434
  runtime_log = (logs_dir / "space_logs_runtime.txt").read_text(encoding="utf-8", errors="ignore") if (logs_dir / "space_logs_runtime.txt").exists() else ""
1435
  repair_dir = run_dir / "repair"
1436
+ repair_dir.mkdir(parents=True, exist_ok=True)
1437
+ classification = classify_repair_failure(failure_reason, build_log, runtime_log)
1438
+ append_event(events_path, "failure_diagnosis", "started", "Preparing Pi blockage diagnosis from Space status and logs", {"iteration": iteration, **classification})
1439
+ brief = write_incident_brief(
1440
+ workspace, run_dir,
1441
+ target_space_id=target_space_id, model_id=model_id, pi_model=pi_model,
1442
+ failure_reason=failure_reason, build_log=build_log, runtime_log=runtime_log,
1443
+ classification=classification, implementation_mode=implementation_mode, expected_output_type=expected_output_type,
1444
+ iteration=iteration, budgets=budgets,
1445
+ )
1446
+ goal = f"""You are Pi in BLOCKAGE DIAGNOSIS MODE for Agentic Space Factory.
1447
 
1448
+ First read `INCIDENT_BRIEF.md` and the HF Spaces gist operational rules: {GIST_URL}
1449
+
1450
+ You are not allowed to edit code in this diagnosis step. Your task is to decide the next action for the Factory.
1451
+ Use the gist method: read logs first, identify the first actionable error, use the cheapest useful iteration rung, and require a live Gradio/API validation before success.
1452
 
1453
+ Write `REPAIR_DECISION.json` exactly as requested in INCIDENT_BRIEF.md. Do not patch files during this step.
1454
+ """
1455
+ (workspace / "PI_DIAGNOSIS_GOAL.md").write_text(goal, encoding="utf-8")
1456
+ (repair_dir / "PI_DIAGNOSIS_GOAL.md").write_text(goal, encoding="utf-8")
1457
+ append_event(events_path, "pi_diagnosis", "started", "Running Pi diagnosis decision before any repair action", {"model": pi_model, "iteration": iteration})
1458
+ write_agent_trace_record(run_dir, phase="diagnosis", event="command_started", status="started", message="Pi blockage diagnosis started", data={"model": pi_model, "iteration": iteration}, artifacts=["repair/INCIDENT_BRIEF.md", "repair/PI_DIAGNOSIS_GOAL.md"])
1459
+ code, out = run_cmd(["pi", "-p", goal], cwd=workspace, timeout=900)
1460
+ logs_dir.mkdir(parents=True, exist_ok=True)
1461
+ (logs_dir / "pi_diagnosis_output.txt").write_text(out, encoding="utf-8")
1462
+ append_agent_trace_artifact(run_dir, phase="diagnosis", event="diagnosis_output", artifact="logs/pi_diagnosis_output.txt", text=out, status="success" if code == 0 else "failed", data={"returncode": code})
1463
+ raw_decision = {}
1464
+ decision_path = workspace / "REPAIR_DECISION.json"
1465
+ if decision_path.exists():
1466
+ raw_decision = load_json_if_exists(decision_path)
1467
+ if not raw_decision:
1468
+ raw_decision = extract_json_object(out)
1469
+ if code != 0:
1470
+ append_event(events_path, "pi_diagnosis", "warning", "Pi diagnosis returned non-zero; using conservative factory fallback", {"returncode": code, "output_tail": out[-3000:]})
1471
+ if not raw_decision:
1472
+ raw_decision = fallback_decision_from_classifier(classification, budgets)
1473
+ decision = normalize_repair_decision(raw_decision, classification, budgets)
1474
+ write_json(repair_dir / "REPAIR_DECISION.json", decision)
1475
+ (workspace / "REPAIR_DECISION.json").write_text(json.dumps(decision, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1476
+ write_agent_trace_record(run_dir, phase="diagnosis", event="repair_decision", status="success", message=f"Pi diagnosis selected action: {decision.get('action')}", data={"decision": decision}, artifacts=["repair/REPAIR_DECISION.json"])
1477
+ append_event(events_path, "repair_decision", "success", f"Pi diagnosis selected action: {decision.get('action')}", {"decision": decision})
1478
+ return decision
1479
+
1480
+
1481
+ def write_blockage_artifact(workspace: Path, run_dir: Path, events_path: Path, decision: dict, failure_reason: str, *, status: str = "technical_blocker"):
1482
+ blocker = {
1483
+ "full_inference_implemented": False,
1484
+ "source": "agentic_space_factory_blockage_protocol",
1485
+ "status": status,
1486
+ "decision": decision,
1487
+ "failure_reason": failure_reason[:4000],
1488
+ "blockers": [
1489
+ {
1490
+ "type": decision.get("action") or status,
1491
+ "claim": decision.get("reason") or "Automated recovery could not safely continue.",
1492
+ "evidence": decision.get("evidence") or [],
1493
+ "severity": "blocking",
1494
+ }
1495
+ ],
1496
+ "suggested_next_step": "Inspect repair/INCIDENT_BRIEF.md and repair/REPAIR_DECISION.json, then retry manually or adjust hardware/settings if appropriate.",
1497
+ }
1498
+ (workspace / "TECHNICAL_BLOCKERS.json").write_text(json.dumps(blocker, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1499
+ gen_dir = run_dir / "generated"
1500
+ gen_dir.mkdir(parents=True, exist_ok=True)
1501
+ (gen_dir / "TECHNICAL_BLOCKERS.json").write_text(json.dumps(blocker, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1502
+ write_json(run_dir / "repair" / "BLOCKAGE.json", blocker)
1503
+ append_event(events_path, "technical_blocker", "failed", "Automated recovery stopped with an auditable blocker", {"decision": decision})
1504
+
1505
+
1506
+ def write_repair_brief(workspace: Path, run_dir: Path, *, target_space_id: str, model_id: str, pi_model: str, failure_reason: str, build_log: str, runtime_log: str, classification: dict, implementation_mode: str, expected_output_type: str, decision: dict | None = None) -> str:
1507
+ repair_dir = run_dir / "repair"
1508
+ repair_dir.mkdir(parents=True, exist_ok=True)
1509
+ inventory = workspace_file_inventory(workspace)
1510
+ decision = decision or {}
1511
+ brief = f"""# Agentic Space Factory repair brief
1512
+
1513
+ ## Scope
1514
+ This is a structured patch pass for an existing generated Hugging Face Space.
1515
+ A separate Pi diagnosis already decided that `patch_code` is justified. Do not rebuild from scratch unless the current architecture is impossible to boot.
1516
+
1517
+ ## Diagnosis decision
1518
+ ```json
1519
+ {json.dumps(decision, indent=2, ensure_ascii=False)}
1520
+ ```
1521
 
1522
+ ## Original target
1523
+ - Model ID: `{model_id}`
1524
+ - Target Space: `{target_space_id}`
1525
+ - Pi model: `{pi_model}`
1526
+ - Implementation mode: `{implementation_mode}`
1527
+ - Expected output type: `{expected_output_type}`
1528
 
1529
+ ## Failure classification
1530
+ - Category: `{classification.get('category', 'unknown_runtime_error')}`
1531
+ - Logs quality: `{classification.get('logs_quality', 'partial')}`
1532
+ - Failure phase: `{classification.get('failure_phase', 'unknown')}`
1533
+ - Recommended strategy: {classification.get('recommendation', '')}
1534
+
1535
+ ## Observed failure
1536
+ ```text
1537
+ {failure_reason[:5000]}
1538
+ ```
1539
+
1540
+ ## Build log tail
1541
  ```text
1542
  {build_log[-12000:]}
1543
  ```
1544
 
1545
+ ## Runtime log tail
1546
  ```text
1547
  {runtime_log[-12000:]}
1548
  ```
1549
 
1550
+ ## Workspace files
1551
+ ```text
1552
+ {chr(10).join(inventory)}
1553
+ ```
 
 
 
 
1554
 
1555
+ ## Repair constraints
1556
+ - Patch the current workspace; do not change the product goal.
1557
+ - Make the smallest patch that fixes the classified failure.
1558
+ - Preserve real inference for Strict inference mode.
1559
+ - Do not replace inference with static placeholders, fake images, canned text, or swallowed exceptions.
1560
+ - Do not change the model ID unless you write a technical blocker explaining why.
1561
+ - Preserve or restore a cheap `health` endpoint.
1562
+ - Preserve the expected Gradio API endpoint when possible.
1563
+ - Keep README metadata valid and short_description <= 60 chars.
1564
+ - Keep Hugging Face Hub requirements modern; do not pin below 1.0 unless unavoidable and explained.
1565
+
1566
+ ## Required repair artifacts
1567
+ Before modifying files, write `REPAIR_PLAN.md` with:
1568
+ - root cause
1569
+ - strategy
1570
+ - files to change
1571
+ - risk level
1572
+ - whether hardware/manual action is required
1573
+
1574
+ After modifying files, write `REPAIR_SUMMARY.md` with:
1575
+ - files changed
1576
+ - why the patch is minimal
1577
+ - how it preserves real inference
1578
+ - how to validate it
1579
+ """
1580
+ (repair_dir / "REPAIR_BRIEF.md").write_text(brief, encoding="utf-8")
1581
+ (workspace / "REPAIR_BRIEF.md").write_text(brief, encoding="utf-8")
1582
+ write_json(repair_dir / "classification.json", classification)
1583
+ return brief
1584
+
1585
+
1586
+ def sanity_check_repair_workspace(workspace: Path, implementation_mode: str) -> tuple[bool, str]:
1587
+ app_path = workspace / "app.py"
1588
+ req_path = workspace / "requirements.txt"
1589
+ readme_path = workspace / "README.md"
1590
+ for required in [app_path, req_path, readme_path]:
1591
+ if not required.exists() or not required.read_text(encoding="utf-8", errors="ignore").strip():
1592
+ return False, f"Repair removed or emptied required file: {required.name}"
1593
+ if implementation_mode == "full-inference-gated":
1594
+ app_text = app_path.read_text(encoding="utf-8", errors="ignore").lower()
1595
+ suspicious = ["placeholder", "dummy image", "fake inference", "todo: replace", "not implemented", "canned response"]
1596
+ if any(marker in app_text for marker in suspicious):
1597
+ return False, "Strict inference repair appears to introduce placeholder/fake inference markers."
1598
+ return True, "Repair workspace sanity checks passed."
1599
+
1600
+
1601
+ def repair_workspace_with_pi(workspace: Path, run_dir: Path, events_path: Path, pi_model: str, target_space_id: str, model_id: str, failure_reason: str, implementation_mode: str = "full-inference-gated", expected_output_type: str = "any", decision: dict | None = None):
1602
+ """Structured one-shot patch pass, only after Pi diagnosis chooses patch_code."""
1603
+ logs_dir = run_dir / "logs"
1604
+ build_log = (logs_dir / "space_logs_build.txt").read_text(encoding="utf-8", errors="ignore") if (logs_dir / "space_logs_build.txt").exists() else ""
1605
+ runtime_log = (logs_dir / "space_logs_runtime.txt").read_text(encoding="utf-8", errors="ignore") if (logs_dir / "space_logs_runtime.txt").exists() else ""
1606
+ repair_dir = run_dir / "repair"
1607
+ before_dir = repair_dir / "before"
1608
+ after_dir = repair_dir / "after"
1609
+ if before_dir.exists():
1610
+ shutil.rmtree(before_dir)
1611
+ repair_dir.mkdir(parents=True, exist_ok=True)
1612
+ shutil.copytree(workspace, before_dir, ignore=shutil.ignore_patterns(".git", "node_modules", "__pycache__", "*.pyc"))
1613
+
1614
+ classification = (decision or {}).get("classification") or classify_repair_failure(failure_reason, build_log, runtime_log)
1615
+ append_event(events_path, "repair_diagnosis", "success", "Patch repair is allowed by Pi diagnosis", {"category": classification.get("category"), "decision": decision or {}})
1616
+ brief = write_repair_brief(
1617
+ workspace, run_dir,
1618
+ target_space_id=target_space_id, model_id=model_id, pi_model=pi_model,
1619
+ failure_reason=failure_reason, build_log=build_log, runtime_log=runtime_log,
1620
+ classification=classification, implementation_mode=implementation_mode, expected_output_type=expected_output_type,
1621
+ decision=decision,
1622
+ )
1623
+ append_event(events_path, "repair_brief", "success", "Repair brief generated for minimal patch", {"category": classification.get("category"), "brief": "repair/REPAIR_BRIEF.md"})
1624
+
1625
+ goal = f"""You are Pi in STRUCTURED REPAIR MODE / STRUCTURED PATCH REPAIR MODE for Agentic Space Factory.
1626
+
1627
+ First read `REPAIR_BRIEF.md`, `INCIDENT_BRIEF.md`, and the HF Spaces gist operational rules: {GIST_URL}
1628
+
1629
+ This is not a new build. This patch is allowed only because the diagnosis decision selected `patch_code`.
1630
+
1631
+ Critical method:
1632
+ 1. Diagnose the root cause from the brief and current files.
1633
+ 2. Write `REPAIR_PLAN.md` before editing files.
1634
+ 3. Apply the smallest patch possible.
1635
+ 4. Write `REPAIR_SUMMARY.md` after editing.
1636
+ 5. Preserve real inference. Never fake outputs just to satisfy validation.
1637
+
1638
+ Failure category: {classification.get('category')}
1639
+ Recommended strategy: {classification.get('recommendation')}
1640
+
1641
+ Hard constraints:
1642
+ - Do not rebuild from scratch unless you clearly justify it in REPAIR_PLAN.md.
1643
+ - Do not change MODEL_ID `{model_id}` unless you write a technical blocker.
1644
+ - Do not remove the expected Gradio/API contract.
1645
+ - For Strict inference, do not replace inference with placeholders, static sample files, canned text, or broad try/except blocks that hide failures.
1646
+ - Keep or restore a cheap health endpoint with api_name="health".
1647
+ - Keep README metadata valid.
1648
+ - Do not publish anything. Work only in the current workspace.
1649
+
1650
+ Required deliverables: REPAIR_PLAN.md, patched files, REPAIR_SUMMARY.md.
1651
  """
1652
  (workspace / "REPAIR_GOAL.md").write_text(goal, encoding="utf-8")
1653
+ (repair_dir / "REPAIR_GOAL.md").write_text(goal, encoding="utf-8")
1654
+ append_event(events_path, "repair_plan", "started", "Running Pi minimal patch repair", {"model": pi_model, "category": classification.get("category")})
1655
+ write_agent_trace_record(run_dir, phase="repair_patch", event="command_started", status="started", message="Pi structured patch repair started", data={"model": pi_model, "category": classification.get("category"), "decision": decision or {}}, artifacts=["repair/REPAIR_BRIEF.md", "repair/REPAIR_GOAL.md", "repair/REPAIR_DECISION.json"])
1656
  code, out = run_cmd(["pi", "-p", goal], cwd=workspace, timeout=1500)
1657
  logs_dir.mkdir(parents=True, exist_ok=True)
1658
  (logs_dir / "pi_repair_output.txt").write_text(out, encoding="utf-8")
1659
+ append_agent_trace_artifact(run_dir, phase="repair_patch", event="repair_output", artifact="logs/pi_repair_output.txt", text=out, status="success" if code == 0 else "failed", data={"returncode": code})
1660
  if code != 0:
1661
+ append_event(events_path, "repair_patch", "failed", "Pi patch repair returned a non-zero exit code", {"returncode": code, "output_tail": out[-3000:]})
1662
  return False
1663
+
1664
+ plan_path = workspace / "REPAIR_PLAN.md"
1665
+ summary_path = workspace / "REPAIR_SUMMARY.md"
1666
+ if not plan_path.exists():
1667
+ plan_path.write_text(f"# Repair plan\n\nPi did not create a separate plan file. Classified category: `{classification.get('category')}`.\n\nStrategy: {classification.get('recommendation')}\n", encoding="utf-8")
1668
+ if not summary_path.exists():
1669
+ summary_path.write_text("# Repair summary\n\nPi completed a structured repair pass but did not write a separate summary file. See logs/pi_repair_output.txt.\n", encoding="utf-8")
1670
+ shutil.copy2(plan_path, repair_dir / "REPAIR_PLAN.md")
1671
+ shutil.copy2(summary_path, repair_dir / "REPAIR_SUMMARY.md")
1672
+ write_agent_trace_record(run_dir, phase="repair_patch", event="repair_plan_summary", status="success", message="Repair plan and summary artifacts are available", data={}, artifacts=["repair/REPAIR_PLAN.md", "repair/REPAIR_SUMMARY.md"])
1673
+ append_event(events_path, "repair_plan", "success", "Repair plan and summary artifacts are available", {"plan": "repair/REPAIR_PLAN.md", "summary": "repair/REPAIR_SUMMARY.md"})
1674
+
1675
  normalize_requirements_for_modern_hub(workspace, events_path)
1676
+ ok, sanity_message = sanity_check_repair_workspace(workspace, implementation_mode)
1677
+ if not ok:
1678
+ append_event(events_path, "repair_patch", "failed", sanity_message, {"category": classification.get("category")})
1679
+ return False
1680
+ append_event(events_path, "repair_patch", "success", sanity_message, {"category": classification.get("category")})
1681
+
1682
  if after_dir.exists():
1683
  shutil.rmtree(after_dir)
1684
  shutil.copytree(workspace, after_dir, ignore=shutil.ignore_patterns(".git", "node_modules", "__pycache__", "*.pyc"))
1685
+ append_event(events_path, "repair", "success", "Structured repair patch completed; ready to re-upload and revalidate", {"output_tail": out[-3000:], "category": classification.get("category")})
1686
  return True
1687
 
1688
+
1689
+ def recover_after_live_validation_failure(api, workspace: Path, run_dir: Path, events_path: Path, *, pi_model: str, target_space_id: str, model_id: str, token: str, failure_reason: str, implementation_mode: str, expected_output_type: str):
1690
+ """Let Pi diagnose the blockage, then execute one bounded Factory action at a time.
1691
+
1692
+ This is the core blockage protocol. Pi decides among allowed actions, but the
1693
+ Factory validates budgets and refuses unsafe patches without actionable logs.
1694
+ """
1695
+ budgets = {"wait_for_logs": 1, "inspect_more_logs": 1, "factory_rebuild_same_code": 1, "patch_code": 1}
1696
+ current_error = failure_reason
1697
+ collect_space_logs(target_space_id, token, run_dir, events_path)
1698
+ for iteration in range(1, 5):
1699
+ decision = diagnose_failure_with_pi(
1700
+ workspace, run_dir, events_path, pi_model, target_space_id, model_id,
1701
+ current_error, implementation_mode, expected_output_type, iteration, budgets,
1702
+ )
1703
+ action = decision.get("action")
1704
+ if action in {"wait_for_logs", "inspect_more_logs"}:
1705
+ if budgets.get(action, 0) <= 0:
1706
+ decision["factory_overrides"] = list(decision.get("factory_overrides") or []) + [f"budget_exhausted:{action}"]
1707
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error)
1708
+ raise RuntimeError(f"Automated recovery stopped after {action} budget was exhausted")
1709
+ budgets[action] -= 1
1710
+ wait_seconds = int(os.environ.get("SPACE_FACTORY_FAILURE_LOG_WAIT_SECONDS", "45"))
1711
+ append_event(events_path, "wait_for_logs", "started", "Waiting for delayed HF Space logs before changing code", {"seconds": wait_seconds, "decision": decision})
1712
+ time.sleep(max(1, wait_seconds))
1713
+ collect_space_logs(target_space_id, token, run_dir, events_path)
1714
+ append_event(events_path, "wait_for_logs", "success", "Collected Space logs after wait", {"remaining_budget": budgets})
1715
+ current_error = f"{current_error}\n\nAfter wait_for_logs/inspect_more_logs, validation is still considered failed; re-diagnose with refreshed logs."
1716
+ continue
1717
+
1718
+ if action == "factory_rebuild_same_code":
1719
+ if budgets.get(action, 0) <= 0:
1720
+ decision["factory_overrides"] = list(decision.get("factory_overrides") or []) + ["budget_exhausted:factory_rebuild_same_code"]
1721
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error)
1722
+ raise RuntimeError("Automated recovery stopped after factory rebuild budget was exhausted")
1723
+ budgets[action] -= 1
1724
+ append_event(events_path, "factory_rebuild", "started", "Re-uploading the same workspace to force a same-code Space rebuild", {"decision": decision})
1725
+ upload_workspace(api, workspace, target_space_id, token, run_dir, events_path)
1726
+ append_event(events_path, "factory_rebuild", "success", "Same-code workspace re-uploaded; revalidating live Space")
1727
+ append_event(events_path, "repair_validation", "started", "Revalidating after same-code factory rebuild")
1728
+ try:
1729
+ validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
1730
+ append_event(events_path, "repair_validation", "success", "Same-code factory rebuild resolved the blockage")
1731
+ return validation
1732
+ except Exception as exc:
1733
+ current_error = f"{current_error}\n\nSame-code factory rebuild did not resolve validation: {str(exc)[:4000]}"
1734
+ collect_space_logs(target_space_id, token, run_dir, events_path)
1735
+ append_event(events_path, "repair_validation", "failed", "Same-code factory rebuild did not resolve validation; re-diagnosing", {"error": str(exc)[:4000]})
1736
+ continue
1737
+
1738
+ if action == "patch_code":
1739
+ if budgets.get(action, 0) <= 0:
1740
+ decision["factory_overrides"] = list(decision.get("factory_overrides") or []) + ["budget_exhausted:patch_code"]
1741
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error)
1742
+ raise RuntimeError("Automated recovery stopped after patch budget was exhausted")
1743
+ budgets[action] -= 1
1744
+ append_event(events_path, "repair", "started", "Pi diagnosis allows a minimal code patch", {"decision": decision})
1745
+ repaired = repair_workspace_with_pi(workspace, run_dir, events_path, pi_model, target_space_id, model_id, current_error, implementation_mode, expected_output_type, decision=decision)
1746
+ if not repaired:
1747
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error, status="failed_after_repair")
1748
+ append_event(events_path, "failure", "failed", "Structured patch repair failed before redeploy", {"decision": decision})
1749
+ raise RuntimeError("Structured patch repair failed before redeploy")
1750
+ append_event(events_path, "repair_upload", "started", "Uploading repaired workspace")
1751
+ upload_workspace(api, workspace, target_space_id, token, run_dir, events_path)
1752
+ append_event(events_path, "repair_upload", "success", "Repaired workspace uploaded")
1753
+ append_event(events_path, "repair_validation", "started", "Revalidating repaired Space")
1754
+ try:
1755
+ validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
1756
+ append_event(events_path, "repair_validation", "success", "Repaired Space passed live API validation")
1757
+ return validation
1758
+ except Exception as exc:
1759
+ current_error = f"{current_error}\n\nPatch repair did not resolve validation: {str(exc)[:4000]}"
1760
+ collect_space_logs(target_space_id, token, run_dir, events_path)
1761
+ append_event(events_path, "repair_validation", "failed", "Repair attempted, but validation still failed", {"error": str(exc)[:4000]})
1762
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error, status="failed_after_repair")
1763
+ append_event(events_path, "failure", "failed", "Run failed after structured repair attempt", {"repair_error": str(exc)[:4000]})
1764
+ raise
1765
+
1766
+ if action == "request_manual_hardware":
1767
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error, status="manual_hardware_required")
1768
+ append_event(events_path, "manual_hardware_required", "failed", "Pi diagnosis requested manual hardware or quota action", {"decision": decision})
1769
+ raise RuntimeError("Pi diagnosis requested manual hardware or quota action")
1770
+
1771
+ write_blockage_artifact(workspace, run_dir, events_path, decision, current_error)
1772
+ append_event(events_path, "failure", "failed", "Pi diagnosis declared a technical blocker", {"decision": decision})
1773
+ raise RuntimeError("Pi diagnosis declared a technical blocker")
1774
+
1775
+ final_decision = {"action": "declare_technical_blocker", "reason": "Blockage protocol exhausted all bounded iterations.", "evidence": [], "patch_allowed": False}
1776
+ write_blockage_artifact(workspace, run_dir, events_path, final_decision, current_error)
1777
+ append_event(events_path, "failure", "failed", "Blockage protocol exhausted all bounded iterations")
1778
+ raise RuntimeError("Blockage protocol exhausted all bounded iterations")
1779
+
1780
  def upload_workspace(api, workspace: Path, target_space_id: str, token: str, run_dir: Path, events_path: Path):
1781
  sanitize_readme_metadata(workspace, events_path)
1782
  normalize_requirements_for_modern_hub(workspace, events_path)
 
1793
  repo_id=target_space_id,
1794
  repo_type="space",
1795
  token=token,
1796
+ ignore_patterns=[".git/*", "node_modules/*", "__pycache__/*", "*.pyc", "GOAL.md", "REPAIR_GOAL.md", "REPAIR_BRIEF.md", "REPAIR_PLAN.md", "REPAIR_SUMMARY.md"],
1797
  )
1798
  uploaded_files = sorted(str(p.relative_to(workspace)) for p in workspace.rglob("*") if p.is_file() and "node_modules" not in p.parts and "__pycache__" not in p.parts)
1799
  append_event(events_path, "upload_files", "success", "Uploaded generated workspace folder", {"file_count": len(uploaded_files), "files_sample": uploaded_files[:50]})
 
1961
  events_path=events_path,
1962
  run_dir=run_dir,
1963
  step="pi_run",
1964
+ trace_phase="initial_build",
1965
  )
1966
  (run_dir / "logs" / "pi_output.txt").write_text(pi_out, encoding="utf-8")
1967
  if code != 0:
 
2012
  try:
2013
  validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
2014
  except Exception as validation_error:
2015
+ append_event(events_path, "failure_detected", "warning", "Initial live validation failed; entering Pi blockage diagnosis protocol", {"error": str(validation_error)[:2000]})
2016
+ validation = recover_after_live_validation_failure(
2017
+ api, workspace, run_dir, events_path,
2018
+ pi_model=pi_model, target_space_id=target_space_id, model_id=model_id, token=token,
2019
+ failure_reason=str(validation_error), implementation_mode=implementation_mode, expected_output_type=expected_output_type,
2020
+ )
 
2021
  generation_smoke = None
2022
  if implementation_mode in {"full-inference-gated", "full-inference-attempt"}:
2023
  try: