lovarch-cli 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. lovarch_cli/__init__.py +16 -0
  2. lovarch_cli/__main__.py +10 -0
  3. lovarch_cli/ai/__init__.py +21 -0
  4. lovarch_cli/ai/gateway.py +240 -0
  5. lovarch_cli/api.py +111 -0
  6. lovarch_cli/auth/__init__.py +32 -0
  7. lovarch_cli/auth/keyring_store.py +214 -0
  8. lovarch_cli/auth/local_server.py +165 -0
  9. lovarch_cli/auth/pkce.py +57 -0
  10. lovarch_cli/auth/session.py +189 -0
  11. lovarch_cli/cli.py +262 -0
  12. lovarch_cli/clients/__init__.py +33 -0
  13. lovarch_cli/clients/factory.py +54 -0
  14. lovarch_cli/clients/local_client.py +432 -0
  15. lovarch_cli/clients/lovarch_storage.py +174 -0
  16. lovarch_cli/clients/lovarch_supabase.py +295 -0
  17. lovarch_cli/clients/persistence.py +166 -0
  18. lovarch_cli/clients/storage.py +66 -0
  19. lovarch_cli/commands/__init__.py +10 -0
  20. lovarch_cli/commands/account.py +172 -0
  21. lovarch_cli/commands/audit.py +394 -0
  22. lovarch_cli/commands/config_cmd.py +80 -0
  23. lovarch_cli/commands/consolidate.py +217 -0
  24. lovarch_cli/commands/context_cmd.py +73 -0
  25. lovarch_cli/commands/dev.py +287 -0
  26. lovarch_cli/commands/do_cmd.py +120 -0
  27. lovarch_cli/commands/init.py +218 -0
  28. lovarch_cli/commands/jobs_cmd.py +95 -0
  29. lovarch_cli/commands/login.py +202 -0
  30. lovarch_cli/commands/mcp_cmd.py +26 -0
  31. lovarch_cli/commands/run.py +375 -0
  32. lovarch_cli/commands/signup.py +185 -0
  33. lovarch_cli/commands/status.py +243 -0
  34. lovarch_cli/commands/upgrade.py +108 -0
  35. lovarch_cli/commands/verifica_cmd.py +174 -0
  36. lovarch_cli/config.py +101 -0
  37. lovarch_cli/config_store.py +111 -0
  38. lovarch_cli/credits/__init__.py +35 -0
  39. lovarch_cli/credits/base.py +84 -0
  40. lovarch_cli/credits/factory.py +36 -0
  41. lovarch_cli/credits/local.py +34 -0
  42. lovarch_cli/credits/lovarch.py +56 -0
  43. lovarch_cli/i18n/__init__.py +27 -0
  44. lovarch_cli/i18n/loader.py +121 -0
  45. lovarch_cli/i18n/translations/en.json +168 -0
  46. lovarch_cli/i18n/translations/es.json +168 -0
  47. lovarch_cli/i18n/translations/it.json +168 -0
  48. lovarch_cli/i18n/translations/pt.json +168 -0
  49. lovarch_cli/mcp/__init__.py +9 -0
  50. lovarch_cli/mcp/server.py +199 -0
  51. lovarch_cli/mcp/tools.py +372 -0
  52. lovarch_cli/sample_downloader.py +255 -0
  53. lovarch_cli/squad/README.md +206 -0
  54. lovarch_cli/squad/agents/auditor-input.md +353 -0
  55. lovarch_cli/squad/agents/bim-engineer.md +404 -0
  56. lovarch_cli/squad/agents/briefing-architect.md +249 -0
  57. lovarch_cli/squad/agents/cad-engineer.md +278 -0
  58. lovarch_cli/squad/agents/capitolato-writer.md +256 -0
  59. lovarch_cli/squad/agents/computo-engineer.md +258 -0
  60. lovarch_cli/squad/agents/concept-designer.md +399 -0
  61. lovarch_cli/squad/agents/contratto-architect.md +243 -0
  62. lovarch_cli/squad/agents/deliverable-builder.md +253 -0
  63. lovarch_cli/squad/agents/energy-prelim.md +388 -0
  64. lovarch_cli/squad/agents/pratiche-it.md +251 -0
  65. lovarch_cli/squad/agents/progetto-chief.md +768 -0
  66. lovarch_cli/squad/agents/quality-dati.md +409 -0
  67. lovarch_cli/squad/agents/quality-misure.md +418 -0
  68. lovarch_cli/squad/agents/quality-normativa.md +417 -0
  69. lovarch_cli/squad/agents/quality-output.md +436 -0
  70. lovarch_cli/squad/agents/regolatorio-it.md +278 -0
  71. lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
  72. lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
  73. lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
  74. lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
  75. lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
  76. lovarch_cli/squad/config.yaml +408 -0
  77. lovarch_cli/squad/data/CHANGELOG.md +272 -0
  78. lovarch_cli/squad/data/agents-prd.md +428 -0
  79. lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
  80. lovarch_cli/squad/data/handoff-card-template.md +231 -0
  81. lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
  82. lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
  83. lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
  84. lovarch_cli/squad/scripts/api_clients.py +206 -0
  85. lovarch_cli/squad/scripts/architect_profile.py +276 -0
  86. lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
  87. lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
  88. lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
  89. lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
  90. lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
  91. lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
  92. lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
  93. lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
  94. lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
  95. lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
  96. lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
  97. lovarch_cli/squad/scripts/validate-squad.py +383 -0
  98. lovarch_cli/squad/tasks/audit-input.md +146 -0
  99. lovarch_cli/squad/tasks/compute-metric.md +105 -0
  100. lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
  101. lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
  102. lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
  103. lovarch_cli/squad/tasks/write-capitolato.md +100 -0
  104. lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
  105. lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
  106. lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
  107. lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
  108. lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
  109. lovarch_cli/squad_loader.py +114 -0
  110. lovarch_cli/verify/__init__.py +15 -0
  111. lovarch_cli/verify/contratto.py +110 -0
  112. lovarch_cli/verify/dossier.py +97 -0
  113. lovarch_cli/verify/misure.py +83 -0
  114. lovarch_cli/verify/normativa.py +178 -0
  115. lovarch_cli/version.py +13 -0
  116. lovarch_cli/workflows/__init__.py +9 -0
  117. lovarch_cli/workflows/platform.py +212 -0
  118. lovarch_cli-0.2.1.dist-info/METADATA +232 -0
  119. lovarch_cli-0.2.1.dist-info/RECORD +122 -0
  120. lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
  121. lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
  122. lovarch_cli-0.2.1.dist-info/licenses/LICENSE +38 -0
@@ -0,0 +1,515 @@
1
+ """
2
+ Simulate Squad Architettura-Progetto execution by inserting realistic steps
3
+ into Supabase tables progressively.
4
+
5
+ Use cases:
6
+ 1. Test the /admin/squad-execution/:id/live page (Realtime)
7
+ 2. Backup demo if real squad execution fails on stage
8
+ 3. Demo previews / dev iteration
9
+
10
+ Usage:
11
+ # 1. Set env vars or write secrets in ~/.lovarch/secrets.env:
12
+ # SUPABASE_URL=https://xxx.supabase.co
13
+ # SUPABASE_SERVICE_ROLE_KEY=eyJ...
14
+ #
15
+ # 2. Run:
16
+ python3 simulate_squad_execution.py
17
+ python3 simulate_squad_execution.py --speed 2.0 # 2x faster
18
+ python3 simulate_squad_execution.py --user-id <uuid>
19
+ python3 simulate_squad_execution.py --dry-run
20
+
21
+ The simulator creates 1 execution + ~30 steps + ~20 QA checks across 14 minutes
22
+ (or compressed by --speed factor). It mimics the dal-brief-al-cantiere workflow
23
+ including 1 deliberate QA REJECT + retry to demonstrate the loop.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import os
30
+ import random
31
+ import sys
32
+ import time
33
+ import urllib.error
34
+ import urllib.request
35
+ import uuid
36
+ from dataclasses import dataclass, field
37
+ from pathlib import Path
38
+ from typing import Any, Dict, List, Optional
39
+
40
+
41
+ SECRETS_FILE = Path.home() / ".lovarch" / "secrets.env"
42
+
43
+
44
+ def load_secrets() -> Dict[str, str]:
45
+ secrets: Dict[str, str] = {}
46
+ if SECRETS_FILE.exists():
47
+ for line in SECRETS_FILE.read_text(encoding="utf-8").splitlines():
48
+ line = line.strip()
49
+ if not line or line.startswith("#") or "=" not in line:
50
+ continue
51
+ k, v = line.split("=", 1)
52
+ secrets[k.strip()] = v.strip()
53
+ return secrets
54
+
55
+
56
+ _SECRETS = load_secrets()
57
+
58
+
59
+ def get_env(name: str, fallback: Optional[str] = None) -> Optional[str]:
60
+ return os.environ.get(name) or _SECRETS.get(name) or fallback
61
+
62
+
63
+ SUPABASE_URL = get_env("SUPABASE_URL")
64
+ SUPABASE_KEY = get_env("SUPABASE_SERVICE_ROLE_KEY") or get_env("SUPABASE_SERVICE_KEY")
65
+
66
+
67
+ # ----------------------------------------------------------------------------
68
+ # Supabase REST client (no SDK · stdlib only)
69
+ # ----------------------------------------------------------------------------
70
+
71
+ class SupabaseRest:
72
+ def __init__(self, url: str, key: str):
73
+ self.url = url.rstrip("/")
74
+ self.headers = {
75
+ "apikey": key,
76
+ "Authorization": f"Bearer {key}",
77
+ "Content-Type": "application/json",
78
+ "Prefer": "return=representation",
79
+ }
80
+
81
+ def insert(self, table: str, row: dict) -> dict:
82
+ return self._req("POST", f"/rest/v1/{table}", body=row)
83
+
84
+ def update(self, table: str, where: str, patch: dict) -> dict:
85
+ return self._req("PATCH", f"/rest/v1/{table}?{where}", body=patch)
86
+
87
+ def select(self, table: str, where: str = "select=*") -> list:
88
+ result = self._req("GET", f"/rest/v1/{table}?{where}")
89
+ return result if isinstance(result, list) else [result]
90
+
91
+ def _req(self, method: str, path: str, body: Optional[dict] = None) -> Any:
92
+ url = f"{self.url}{path}"
93
+ data = json.dumps(body).encode("utf-8") if body is not None else None
94
+ req = urllib.request.Request(url, data=data, method=method, headers=self.headers)
95
+ try:
96
+ with urllib.request.urlopen(req, timeout=15) as r:
97
+ raw = r.read().decode("utf-8") or "[]"
98
+ return json.loads(raw)
99
+ except urllib.error.HTTPError as e:
100
+ err = e.read().decode("utf-8", errors="replace")
101
+ raise RuntimeError(f"Supabase {method} {path} failed [{e.code}]: {err}") from e
102
+
103
+
104
+ # ----------------------------------------------------------------------------
105
+ # Step definitions · simulating the workflow
106
+ # ----------------------------------------------------------------------------
107
+
108
+ @dataclass
109
+ class StepDef:
110
+ agent: str
111
+ tier: int
112
+ step_type: str
113
+ duration: float
114
+ action: str
115
+ output_files: List[Dict[str, Any]] = field(default_factory=list)
116
+ qa_outcome: Optional[str] = None # "PASS" | "REJECT"
117
+ qa_checks: List[Dict[str, Any]] = field(default_factory=list)
118
+
119
+
120
+ def _file(name: str, path: str, size_kb: int, mime: str = "application/pdf") -> dict:
121
+ return {
122
+ "name": name,
123
+ "path": path,
124
+ "size": size_kb * 1024,
125
+ "mime": mime,
126
+ }
127
+
128
+
129
+ # Workflow simulation · 28 steps + 1 QA REJECT + retry
130
+ WORKFLOW: List[StepDef] = [
131
+ # TIER 0 · ORCHESTRAZIONE
132
+ StepDef("@progetto-chief", 0, "orchestration", 4, "init: caricato briefing-cliente.md (1.4k parole)"),
133
+ StepDef("@auditor-input", 0, "orchestration", 12, "validazione 18 items input · PASS",
134
+ output_files=[_file("input_validation.json", "00-validation/", 2, "application/json")]),
135
+
136
+ # TIER 1 · ESECUZIONE PARALLELA
137
+ StepDef("@briefing-architect", 1, "execution", 28, "brief strutturato 12 sezioni UNI 11337",
138
+ output_files=[
139
+ _file("brief-strutturato.pdf", "01-briefing/", 412),
140
+ _file("requisiti.json", "01-briefing/", 4, "application/json"),
141
+ _file("programma-spaziale.xlsx", "01-briefing/", 22, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
142
+ ]),
143
+ StepDef("@regolatorio-it", 1, "execution", 38, "tipo pratica = CILA · paesaggistica DPR 31/2017 procedura semplificata",
144
+ output_files=[
145
+ _file("analisi-regolamentare.pdf", "01-briefing/", 624),
146
+ _file("tipo-pratica.json", "01-briefing/", 3, "application/json"),
147
+ _file("vincoli.json", "01-briefing/", 2, "application/json"),
148
+ ]),
149
+ StepDef("@cad-engineer", 1, "execution", 95, "pianta-progetto.dxf · 8 ambienti · cotazioni UNI ISO 5457",
150
+ output_files=[
151
+ _file("pianta-stato-attuale.dxf", "03-progetto-definitivo/", 78, "application/dxf"),
152
+ _file("pianta-stato-attuale.pdf", "03-progetto-definitivo/", 312),
153
+ _file("pianta-progetto.dxf", "03-progetto-definitivo/", 96, "application/dxf"),
154
+ _file("pianta-progetto.pdf", "03-progetto-definitivo/", 387),
155
+ _file("sezione-AA.pdf", "03-progetto-definitivo/", 218),
156
+ _file("prospetti.pdf", "03-progetto-definitivo/", 442),
157
+ _file("schema-quotato.json", "03-progetto-definitivo/", 5, "application/json"),
158
+ ]),
159
+ StepDef("@bim-engineer", 1, "execution", 78, "modello.ifc LOD 300 · viewer APS embeddable",
160
+ output_files=[
161
+ _file("modello.ifc", "03-progetto-definitivo/", 1834, "application/x-step"),
162
+ _file("thumbnail-3d.png", "03-progetto-definitivo/", 412, "image/png"),
163
+ _file("quantitativi.json", "03-progetto-definitivo/", 8, "application/json"),
164
+ ]),
165
+ StepDef("@concept-designer", 1, "execution", 168,
166
+ "moodboard 9 immagini + 6 render FLUX 1.1 Pro + palette + tipografia",
167
+ output_files=[
168
+ _file("moodboard.pdf", "02-concept/", 8420),
169
+ _file("living-moderno-a.png", "02-concept/renders/", 4280, "image/png"),
170
+ _file("living-moderno-b.png", "02-concept/renders/", 4156, "image/png"),
171
+ _file("cucina-moderna-a.png", "02-concept/renders/", 4350, "image/png"),
172
+ _file("cucina-moderna-b.png", "02-concept/renders/", 4120, "image/png"),
173
+ _file("camera-sofia-a.png", "02-concept/renders/", 3980, "image/png"),
174
+ _file("camera-sofia-b.png", "02-concept/renders/", 4042, "image/png"),
175
+ _file("palette-progetto.pdf", "02-concept/", 187),
176
+ _file("tipografia.pdf", "02-concept/", 142),
177
+ ]),
178
+ StepDef("@computo-engineer", 1, "execution", 52,
179
+ "computo metrico estimativo · 124 voci Prezzario Lombardia · totale €180.000",
180
+ output_files=[
181
+ _file("computo-metrico.xlsx", "05-impresa/", 240, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
182
+ _file("computo-metrico.pdf", "05-impresa/", 524),
183
+ _file("quadro-economico.pdf", "05-impresa/", 142),
184
+ _file("lista-materiali-EPDs.xlsx", "05-impresa/", 98, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
185
+ ]),
186
+ StepDef("@capitolato-writer", 1, "execution", 78,
187
+ "capitolato speciale 78 pp · UNI 11337-7 + CAM 2025 · cronoprogramma 90gg",
188
+ output_files=[
189
+ _file("capitolato-speciale.pdf", "05-impresa/", 3420),
190
+ _file("cronoprogramma-90gg.pdf", "05-impresa/", 412),
191
+ _file("lista-CAM-rispettati.xlsx", "05-impresa/", 76, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
192
+ ]),
193
+ StepDef("@pratiche-it", 1, "execution", 42,
194
+ "CILA precompilata + asseverazione + paesaggistica + 6 elaborati",
195
+ output_files=[
196
+ _file("CILA-precompilata.pdf", "04-pratiche-comune/", 512),
197
+ _file("asseverazione-bozza.pdf", "04-pratiche-comune/", 189),
198
+ _file("paesaggistica-bozza.pdf", "04-pratiche-comune/", 734),
199
+ _file("relazione-paesaggistica.pdf", "04-pratiche-comune/", 412),
200
+ _file("documentazione-fotografica.pdf", "04-pratiche-comune/", 12340),
201
+ ]),
202
+ StepDef("@contratto-architect", 1, "execution", 22,
203
+ "contratto CNAPPC + preventivo + privacy GDPR · onorari €22.000",
204
+ output_files=[
205
+ _file("contratto-servizi.pdf", "07-cliente/", 412),
206
+ _file("preventivo-onorari.pdf", "07-cliente/", 298),
207
+ _file("informativa-privacy-GDPR.pdf", "07-cliente/", 156),
208
+ ]),
209
+ StepDef("@energy-prelim", 1, "execution", 36,
210
+ "APE preliminare · classe stimata B → A · LCA embodied carbon",
211
+ output_files=[
212
+ _file("APE-stima-preliminare.pdf", "06-ingegneri/", 682),
213
+ _file("LCA-embodied-carbon.pdf", "06-ingegneri/", 934),
214
+ _file("trasmittanze-pareti.xlsx", "06-ingegneri/", 42, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
215
+ ]),
216
+ StepDef("@deliverable-builder", 1, "execution", 32,
217
+ "presentazione HTML V8 + portale cliente + DOSSIER.zip",
218
+ output_files=[
219
+ _file("presentazione-cliente.html", "07-cliente/", 1240, "text/html"),
220
+ _file("timeline-90gg-cliente.pdf", "07-cliente/", 187),
221
+ _file("DOSSIER-IMPRESA.zip", "05-impresa/", 15240, "application/zip"),
222
+ _file("scheda-progetto.json", "08-studio-interno/", 8, "application/json"),
223
+ _file("cash-flow-proiezione.xlsx", "08-studio-interno/", 42, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
224
+ _file("task-list-team.json", "08-studio-interno/", 12, "application/json"),
225
+ _file("social-instagram.json", "08-studio-interno/", 28, "application/json"),
226
+ ]),
227
+
228
+ # TIER 2 · QA · 1 deliberate REJECT to show the loop
229
+ StepDef("@quality-misure", 2, "qa", 18, "verifica 24 items · 5/5 critici PASS · 9/11 secondari PASS",
230
+ qa_outcome="PASS",
231
+ qa_checks=[
232
+ {"check_id": "C1", "severity": "CRITICO", "description": "Somma quote orizzontali", "result": True},
233
+ {"check_id": "C2", "severity": "CRITICO", "description": "Somma quote verticali", "result": True},
234
+ {"check_id": "C3", "severity": "CRITICO", "description": "Sup utile = somma ambienti", "result": True},
235
+ {"check_id": "C4", "severity": "CRITICO", "description": "Sup lorda = utile + muratura", "result": True},
236
+ {"check_id": "C5", "severity": "CRITICO", "description": "Volume = sup × altezza", "result": True},
237
+ ]),
238
+ StepDef("@quality-normativa", 2, "qa", 22, "verifica 18 items · DPR 380 art 6-bis OK · CAM 2025 89% rispettato",
239
+ qa_outcome="PASS",
240
+ qa_checks=[
241
+ {"check_id": "N-C1", "severity": "CRITICO", "description": "Tipo pratica corretto", "result": True},
242
+ {"check_id": "N-C2", "severity": "CRITICO", "description": "Articoli DPR 380 esistono", "result": True},
243
+ {"check_id": "N-C3", "severity": "CRITICO", "description": "Aut paesaggistica considerata", "result": True},
244
+ {"check_id": "N-C4", "severity": "CRITICO", "description": "CAM Edilizia 2025 ≥80%", "result": True},
245
+ ]),
246
+ StepDef("@quality-dati", 2, "qa", 26,
247
+ "REJECT · Volume parete IFC 18.5 m² ≠ computo 19.2 m² (diff 3.7%)",
248
+ qa_outcome="REJECT",
249
+ qa_checks=[
250
+ {"check_id": "D-C1", "severity": "CRITICO", "description": "Sup lorda pianta=IFC=CILA", "result": True},
251
+ {"check_id": "D-C2", "severity": "CRITICO", "description": "Volumi parete IFC = computo", "result": False,
252
+ "expected_value": "18.5 m²", "actual_value": "19.2 m²", "diff": "+0.7 m² (3.7%)"},
253
+ {"check_id": "D-C3", "severity": "CRITICO", "description": "Totale computo=capitolato=contratto", "result": True},
254
+ ]),
255
+
256
+ # RETRY · @computo-engineer corrige
257
+ StepDef("@computo-engineer", 1, "execution", 12, "RETRY 1/3 · correzione voce muratura demolizione (18.5 m²)",
258
+ output_files=[
259
+ _file("computo-metrico.xlsx", "05-impresa/", 240, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
260
+ ]),
261
+
262
+ # QA RE-RUN
263
+ StepDef("@quality-dati", 2, "qa", 14, "RE-VERIFY · 16/16 items · PASS",
264
+ qa_outcome="PASS",
265
+ qa_checks=[
266
+ {"check_id": "D-C2", "severity": "CRITICO", "description": "Volumi parete IFC = computo (re-check)", "result": True,
267
+ "expected_value": "18.5 m²", "actual_value": "18.5 m²"},
268
+ ]),
269
+ StepDef("@quality-output", 2, "qa", 16, "verifica 14 items · 27 deliverable · tutti i PDF aprono · upload Lovarch OK",
270
+ qa_outcome="PASS",
271
+ qa_checks=[
272
+ {"check_id": "O-C1", "severity": "CRITICO", "description": "25+ deliverable presenti", "result": True,
273
+ "actual_value": "27"},
274
+ {"check_id": "O-C2", "severity": "CRITICO", "description": "PDF aprono senza errore", "result": True,
275
+ "actual_value": "18/18"},
276
+ {"check_id": "O-C6", "severity": "CRITICO", "description": "File uploaded → Lovarch", "result": True,
277
+ "actual_value": "27/27"},
278
+ ]),
279
+
280
+ # TIER 0 · CONSOLIDATION
281
+ StepDef("@progetto-chief", 0, "orchestration", 8, "consolidamento dossier · git commit · open Finder",
282
+ output_files=[
283
+ _file("README.md", "", 4, "text/markdown"),
284
+ _file("manifest.json", "", 6, "application/json"),
285
+ ]),
286
+ ]
287
+
288
+
289
+ # ----------------------------------------------------------------------------
290
+ # Simulator
291
+ # ----------------------------------------------------------------------------
292
+
293
+ def run_simulation(
294
+ *,
295
+ user_id: str,
296
+ project_id: Optional[str] = None,
297
+ speed: float = 1.0,
298
+ dry_run: bool = False,
299
+ sb: Optional[SupabaseRest] = None,
300
+ ) -> Dict[str, Any]:
301
+ """Run the simulation, inserting rows progressively."""
302
+ execution_id = str(uuid.uuid4())
303
+
304
+ if dry_run:
305
+ print(f"[DRY-RUN] would create execution {execution_id} with {len(WORKFLOW)} steps")
306
+ print(f"[DRY-RUN] estimated total duration (real): {sum(s.duration for s in WORKFLOW):.0f}s "
307
+ f"({sum(s.duration for s in WORKFLOW) / speed:.0f}s with speed={speed})")
308
+ return {"execution_id": execution_id, "dry_run": True}
309
+
310
+ if sb is None:
311
+ raise ValueError("SupabaseRest client required when not dry_run")
312
+
313
+ # 1. Create execution
314
+ print(f"\n▶ Creating execution {execution_id[:8]}...")
315
+ exec_row = {
316
+ "id": execution_id,
317
+ "user_id": user_id,
318
+ "project_id": project_id,
319
+ "status": "running",
320
+ "metadata": {
321
+ "simulator": True,
322
+ "scenario": "Attico Brera demo",
323
+ "client": "Marco Rossini & Giulia Bianchi",
324
+ },
325
+ }
326
+ sb.insert("pm_squad_executions", exec_row)
327
+
328
+ # 2. AUTO-OPEN live tracking page in browser (so Pablo can watch real-time)
329
+ base_url = os.environ.get("LOVARCH_WEB_URL", "https://lovarch.com")
330
+ live_url = f"{base_url}/admin/squad-execution/{execution_id}/live"
331
+ print(f" Opening live tracking page: {live_url}")
332
+ try:
333
+ import webbrowser
334
+ webbrowser.open(live_url, new=2) # new=2 = new tab if possible
335
+ except Exception as e:
336
+ print(f" (could not auto-open browser: {e} · open manually)")
337
+
338
+ # 3. Iterate steps
339
+ total_steps = 0
340
+ total_qa_rejects = 0
341
+ total_retries = 0
342
+
343
+ for i, step_def in enumerate(WORKFLOW, start=1):
344
+ step_id = str(uuid.uuid4())
345
+
346
+ # Insert pending step
347
+ step_row = {
348
+ "id": step_id,
349
+ "execution_id": execution_id,
350
+ "agent_name": step_def.agent,
351
+ "tier": step_def.tier,
352
+ "step_type": step_def.step_type,
353
+ "status": "pending",
354
+ "action_description": step_def.action,
355
+ }
356
+ sb.insert("pm_squad_steps", step_row)
357
+
358
+ # Mark as running
359
+ time.sleep(0.5 / speed)
360
+ started_at_ms = int(time.time() * 1000)
361
+ sb.update(
362
+ "pm_squad_steps",
363
+ f"id=eq.{step_id}",
364
+ {
365
+ "status": "running",
366
+ "started_at": _ts(),
367
+ },
368
+ )
369
+ print(f" [{i:2d}/{len(WORKFLOW)}] {step_def.agent:<25} {step_def.action[:60]}...", end="", flush=True)
370
+
371
+ # Wait simulated duration
372
+ time.sleep(step_def.duration / speed)
373
+
374
+ # Determine final status
375
+ if step_def.qa_outcome == "REJECT":
376
+ final_status = "rejected"
377
+ total_qa_rejects += 1
378
+ else:
379
+ final_status = "done"
380
+
381
+ # If this is a retry step, mark with retry_count
382
+ retry_count = 1 if "RETRY" in step_def.action.upper() else 0
383
+ if retry_count:
384
+ total_retries += 1
385
+
386
+ # Compute duration
387
+ elapsed = (int(time.time() * 1000) - started_at_ms) / 1000
388
+
389
+ sb.update(
390
+ "pm_squad_steps",
391
+ f"id=eq.{step_id}",
392
+ {
393
+ "status": final_status,
394
+ "completed_at": _ts(),
395
+ "duration_seconds": int(elapsed),
396
+ "output_files": step_def.output_files,
397
+ "qa_report": {"verdict": step_def.qa_outcome} if step_def.qa_outcome else None,
398
+ "retry_count": retry_count,
399
+ },
400
+ )
401
+
402
+ # Insert QA checks if applicable
403
+ if step_def.qa_checks:
404
+ for chk in step_def.qa_checks:
405
+ sb.insert("pm_squad_qa_checks", {
406
+ "step_id": step_id,
407
+ "execution_id": execution_id,
408
+ "qa_agent": step_def.agent,
409
+ "check_id": chk["check_id"],
410
+ "check_description": chk["description"],
411
+ "severity": chk["severity"],
412
+ "result": chk["result"],
413
+ "expected_value": chk.get("expected_value"),
414
+ "actual_value": chk.get("actual_value"),
415
+ "diff": chk.get("diff"),
416
+ })
417
+
418
+ total_steps += 1
419
+ print(f" {final_status.upper()}")
420
+
421
+ # 4. Mark execution as completed
422
+ sb.update(
423
+ "pm_squad_executions",
424
+ f"id=eq.{execution_id}",
425
+ {
426
+ "status": "completed",
427
+ "completed_at": _ts(),
428
+ "total_steps": total_steps,
429
+ "total_qa_rejects": total_qa_rejects,
430
+ "total_retries": total_retries,
431
+ },
432
+ )
433
+
434
+ # 5. AUTO-OPEN dossier page (final deliverables) when complete
435
+ dossier_url = f"{base_url}/admin/squad-execution/{execution_id}/dossier"
436
+ print(f"\n✓ Execution complete: {execution_id}")
437
+ print(f" Total steps: {total_steps}")
438
+ print(f" QA rejects: {total_qa_rejects} (loop demonstrato)")
439
+ print(f" Retries: {total_retries}")
440
+ print(f"\n Opening dossier page: {dossier_url}")
441
+ try:
442
+ import webbrowser
443
+ webbrowser.open(dossier_url, new=2)
444
+ except Exception:
445
+ pass # already opened live, dossier is bonus
446
+
447
+ return {
448
+ "execution_id": execution_id,
449
+ "user_id": user_id,
450
+ "total_steps": total_steps,
451
+ "total_qa_rejects": total_qa_rejects,
452
+ "total_retries": total_retries,
453
+ "live_url": f"/admin/squad-execution/{execution_id}/live",
454
+ "dossier_url": f"/admin/squad-execution/{execution_id}/dossier",
455
+ }
456
+
457
+
458
+ def _ts() -> str:
459
+ """ISO timestamp in UTC."""
460
+ from datetime import datetime, timezone
461
+ return datetime.now(timezone.utc).isoformat()
462
+
463
+
464
+ # ----------------------------------------------------------------------------
465
+ # CLI
466
+ # ----------------------------------------------------------------------------
467
+
468
+ def main():
469
+ parser = argparse.ArgumentParser(description="Simulate squad execution")
470
+ parser.add_argument("--user-id", default=None, help="Supabase user UUID (defaults to env SUPABASE_USER_ID)")
471
+ parser.add_argument("--project-id", default=None, help="Project UUID (optional)")
472
+ parser.add_argument("--speed", type=float, default=1.0, help="Speed multiplier (1.0 = real time, 5.0 = 5× faster)")
473
+ parser.add_argument("--dry-run", action="store_true", help="Print plan without inserting")
474
+ args = parser.parse_args()
475
+
476
+ if args.dry_run:
477
+ result = run_simulation(
478
+ user_id=args.user_id or "00000000-0000-0000-0000-000000000000",
479
+ project_id=args.project_id,
480
+ speed=args.speed,
481
+ dry_run=True,
482
+ )
483
+ print(json.dumps(result, indent=2))
484
+ return
485
+
486
+ if not SUPABASE_URL or not SUPABASE_KEY:
487
+ sys.stderr.write(
488
+ "ERROR: SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY missing.\n"
489
+ "Add to ~/.lovarch/secrets.env or env vars.\n"
490
+ )
491
+ sys.exit(1)
492
+
493
+ user_id = args.user_id or get_env("SUPABASE_USER_ID")
494
+ if not user_id:
495
+ sys.stderr.write(
496
+ "ERROR: --user-id required (or set SUPABASE_USER_ID).\n"
497
+ "Get yours: SELECT id FROM auth.users WHERE email='pablo@archprime.io';\n"
498
+ )
499
+ sys.exit(1)
500
+
501
+ sb = SupabaseRest(SUPABASE_URL, SUPABASE_KEY)
502
+ result = run_simulation(
503
+ user_id=user_id,
504
+ project_id=args.project_id,
505
+ speed=args.speed,
506
+ sb=sb,
507
+ )
508
+ base_url = os.environ.get("LOVARCH_WEB_URL", "https://lovarch.com")
509
+ print(f"\nLive tracking: {base_url}{result['live_url']}")
510
+ print(f"Dossier: {base_url}{result['dossier_url']}")
511
+ print("\n(Browser was auto-opened on these pages during execution)")
512
+
513
+
514
+ if __name__ == "__main__":
515
+ main()