vqlab 0.1.0__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 (290) hide show
  1. vqlab/__init__.py +79 -0
  2. vqlab/_layout.py +225 -0
  3. vqlab/agents/CONTEXT.md +50 -0
  4. vqlab/agents/__init__.py +1 -0
  5. vqlab/agents/_fetch.py +46 -0
  6. vqlab/agents/bug_report.py +107 -0
  7. vqlab/agents/config_cmd.py +165 -0
  8. vqlab/agents/doctor.py +234 -0
  9. vqlab/agents/downloads.py +255 -0
  10. vqlab/agents/first_run.py +77 -0
  11. vqlab/agents/gui.py +511 -0
  12. vqlab/agents/gui_static/index.html +1605 -0
  13. vqlab/agents/jobs.py +257 -0
  14. vqlab/agents/mcp/__init__.py +15 -0
  15. vqlab/agents/mcp/common.py +8 -0
  16. vqlab/agents/mcp/server.py +290 -0
  17. vqlab/agents/mcp/tools_findings.py +32 -0
  18. vqlab/agents/mcp/tools_runs.py +44 -0
  19. vqlab/agents/mcp/tools_storage.py +38 -0
  20. vqlab/agents/model_scan.py +271 -0
  21. vqlab/agents/onboard.py +339 -0
  22. vqlab/agents/scratch.py +526 -0
  23. vqlab/agents/settings.py +242 -0
  24. vqlab/agents/teachers.py +213 -0
  25. vqlab/agents/updates.py +76 -0
  26. vqlab/api.py +605 -0
  27. vqlab/bench/CONTEXT.md +43 -0
  28. vqlab/bench/__init__.py +1 -0
  29. vqlab/bench/active_bytes.py +213 -0
  30. vqlab/bench/box_quiet.py +70 -0
  31. vqlab/bench/decode_ladder.py +663 -0
  32. vqlab/bench/decode_timeline.py +447 -0
  33. vqlab/bench/gdn_chunk_truth.py +162 -0
  34. vqlab/bench/gpu_capture.py +99 -0
  35. vqlab/bench/hc_micro.py +159 -0
  36. vqlab/bench/host_attrib.py +116 -0
  37. vqlab/bench/kernel_coverage.py +97 -0
  38. vqlab/bench/lut_bench.py +252 -0
  39. vqlab/bench/mtp_accept.py +172 -0
  40. vqlab/bench/mtp_arms.py +59 -0
  41. vqlab/bench/mtp_bench.py +71 -0
  42. vqlab/bench/power.py +23 -0
  43. vqlab/bench/prefill_bench.py +194 -0
  44. vqlab/bench/prefill_timeline.py +159 -0
  45. vqlab/bench/serve_timeline.py +174 -0
  46. vqlab/bench/speed_pair.py +124 -0
  47. vqlab/bench/speed_pair_knurlogic.py +213 -0
  48. vqlab/bench/stage_bandwidth.py +249 -0
  49. vqlab/bench/timeline_common.py +31 -0
  50. vqlab/build/CONTEXT.md +41 -0
  51. vqlab/build/__init__.py +1 -0
  52. vqlab/build/build_dense.py +331 -0
  53. vqlab/build/graft_extras.py +165 -0
  54. vqlab/build/graft_vision.py +322 -0
  55. vqlab/build/loo_bands.py +71 -0
  56. vqlab/build/minibase.py +47 -0
  57. vqlab/build/mix.py +161 -0
  58. vqlab/build/mtp_extract.py +132 -0
  59. vqlab/build/mtp_graft.py +165 -0
  60. vqlab/build/mtp_head_ds4.py +143 -0
  61. vqlab/build/mtp_pack.py +140 -0
  62. vqlab/build/pack.py +288 -0
  63. vqlab/build/pack_dense.py +128 -0
  64. vqlab/build/pack_ple.py +112 -0
  65. vqlab/build/ple_swap.py +121 -0
  66. vqlab/build/reskeleton.py +181 -0
  67. vqlab/build/sanitize_stream.py +157 -0
  68. vqlab/build/slice_teacher.py +241 -0
  69. vqlab/build/splice_ple.py +120 -0
  70. vqlab/build/stream_convert.py +330 -0
  71. vqlab/build/teacher_prep.py +85 -0
  72. vqlab/build/unpack_dense.py +136 -0
  73. vqlab/bundle/CONTEXT.md +22 -0
  74. vqlab/bundle/__init__.py +1 -0
  75. vqlab/bundle/add_model_file.py +333 -0
  76. vqlab/bundle/patch_arch.py +56 -0
  77. vqlab/bundle/rebundle_dense.py +80 -0
  78. vqlab/bundle/vision_layout.py +142 -0
  79. vqlab/cli.py +223 -0
  80. vqlab/commands.py +490 -0
  81. vqlab/config.py +637 -0
  82. vqlab/core/CONTEXT.md +27 -0
  83. vqlab/core/__init__.py +1 -0
  84. vqlab/core/artifacts/CONTEXT.md +17 -0
  85. vqlab/core/artifacts/__init__.py +1 -0
  86. vqlab/core/artifacts/artifact.py +296 -0
  87. vqlab/core/artifacts/artifact_size.py +27 -0
  88. vqlab/core/artifacts/codepack.py +49 -0
  89. vqlab/core/artifacts/hub.py +65 -0
  90. vqlab/core/artifacts/pin_record.py +45 -0
  91. vqlab/core/artifacts/stamp_fields.py +40 -0
  92. vqlab/core/box/CONTEXT.md +15 -0
  93. vqlab/core/box/__init__.py +1 -0
  94. vqlab/core/box/arch_finder.py +104 -0
  95. vqlab/core/box/exo.py +26 -0
  96. vqlab/core/box/gpu_lease.py +109 -0
  97. vqlab/core/box/knurlogic_client.py +151 -0
  98. vqlab/core/source/CONTEXT.md +18 -0
  99. vqlab/core/source/__init__.py +1 -0
  100. vqlab/core/source/clamps.py +42 -0
  101. vqlab/core/source/expert_src.py +168 -0
  102. vqlab/core/source/fit_store.py +473 -0
  103. vqlab/core/source/mem_budget.py +83 -0
  104. vqlab/core/source/ple_stream.py +196 -0
  105. vqlab/core/source/teacher_profiles.py +74 -0
  106. vqlab/family/CONTEXT.md +30 -0
  107. vqlab/family/__init__.py +155 -0
  108. vqlab/family/arch_equivalence.json +33 -0
  109. vqlab/family/deepseek_v4.py +269 -0
  110. vqlab/family/gemma4.py +187 -0
  111. vqlab/family/glm5_next.py +215 -0
  112. vqlab/family/model_loader.py +213 -0
  113. vqlab/family/qwen3_5.py +159 -0
  114. vqlab/family/qwen4_exp.py +123 -0
  115. vqlab/family/registry.py +317 -0
  116. vqlab/family/version_stamp.py +419 -0
  117. vqlab/fit/CONTEXT.md +35 -0
  118. vqlab/fit/__init__.py +1 -0
  119. vqlab/fit/additive_codebooks.py +98 -0
  120. vqlab/fit/fit_additive.py +224 -0
  121. vqlab/fit/fit_dense_vq.py +257 -0
  122. vqlab/fit/fit_moe.py +704 -0
  123. vqlab/fit/fit_ple.py +189 -0
  124. vqlab/fit/fits.py +216 -0
  125. vqlab/fit/geo_build.py +664 -0
  126. vqlab/fit/harvest_parts.py +142 -0
  127. vqlab/fit/kmeans.py +290 -0
  128. vqlab/fit/reselect.py +526 -0
  129. vqlab/gate/CONTEXT.md +36 -0
  130. vqlab/gate/__init__.py +1 -0
  131. vqlab/gate/bundle_accept.py +109 -0
  132. vqlab/gate/check_all.py +99 -0
  133. vqlab/gate/check_bundle.py +391 -0
  134. vqlab/gate/check_comparator.py +94 -0
  135. vqlab/gate/check_release.py +756 -0
  136. vqlab/gate/kernel_accept.py +118 -0
  137. vqlab/gate/kernel_reference.py +23 -0
  138. vqlab/gate/knurlogic_link.py +103 -0
  139. vqlab/gate/leak_guard.py +131 -0
  140. vqlab/gate/mtp_smoke_head.py +99 -0
  141. vqlab/gate/parity.py +125 -0
  142. vqlab/gate/pin.py +184 -0
  143. vqlab/gate/smoke.py +313 -0
  144. vqlab/gate/spelling.py +167 -0
  145. vqlab/gate/tokenizer_match.py +75 -0
  146. vqlab/gate/verify_artifact.py +209 -0
  147. vqlab/gate/verify_dev_codebook.py +100 -0
  148. vqlab/gate/vision_smoke.py +571 -0
  149. vqlab/mtp/CONTEXT.md +36 -0
  150. vqlab/mtp/__init__.py +30 -0
  151. vqlab/mtp/bench.py +108 -0
  152. vqlab/mtp/caches.py +120 -0
  153. vqlab/mtp/capture.py +68 -0
  154. vqlab/mtp/cli_common.py +40 -0
  155. vqlab/mtp/graft.py +56 -0
  156. vqlab/mtp/loop.py +541 -0
  157. vqlab/mtp/mtp_head.py +219 -0
  158. vqlab/mtp/mtp_head_glm5.py +244 -0
  159. vqlab/mtp/mtp_head_qwen35.py +264 -0
  160. vqlab/mtp/registry.py +241 -0
  161. vqlab/mtp/runtime.py +75 -0
  162. vqlab/mtp/sampling.py +145 -0
  163. vqlab/ops/CONTEXT.md +41 -0
  164. vqlab/ops/__init__.py +20 -0
  165. vqlab/ops/box.py +52 -0
  166. vqlab/ops/docs.py +42 -0
  167. vqlab/ops/errors.py +21 -0
  168. vqlab/ops/findings.py +223 -0
  169. vqlab/ops/queues.py +47 -0
  170. vqlab/ops/roots.py +57 -0
  171. vqlab/ops/runs.py +208 -0
  172. vqlab/ops/storage.py +120 -0
  173. vqlab/ops/supervisor.py +106 -0
  174. vqlab/plan/CONTEXT.md +32 -0
  175. vqlab/plan/__init__.py +1 -0
  176. vqlab/plan/alloc_sweep.py +260 -0
  177. vqlab/plan/family_profile.py +401 -0
  178. vqlab/plan/init_sweep.py +187 -0
  179. vqlab/plan/layer_leverage.py +346 -0
  180. vqlab/plan/mtp_probe_qwen35.py +185 -0
  181. vqlab/plan/mtp_probe_qwen4_exp.py +234 -0
  182. vqlab/plan/preflight_disk.py +73 -0
  183. vqlab/plan/preflight_ram.py +75 -0
  184. vqlab/plan/price.py +167 -0
  185. vqlab/plan/recipe_queue.py +940 -0
  186. vqlab/plan/zero_groups.py +186 -0
  187. vqlab/queue/CONTEXT.md +53 -0
  188. vqlab/queue/__init__.py +5 -0
  189. vqlab/queue/eta.py +169 -0
  190. vqlab/queue/files.py +175 -0
  191. vqlab/queue/remote.py +187 -0
  192. vqlab/queue/report.py +126 -0
  193. vqlab/queue/reserve.py +269 -0
  194. vqlab/queue/runner.py +457 -0
  195. vqlab/queue/steps.py +322 -0
  196. vqlab/queue/validate.py +228 -0
  197. vqlab/records/CONTEXT.md +24 -0
  198. vqlab/records/__init__.py +1 -0
  199. vqlab/records/artifact_manifest.py +103 -0
  200. vqlab/records/provenance.py +678 -0
  201. vqlab/records/registry.py +267 -0
  202. vqlab/records/runlog.py +119 -0
  203. vqlab/records/scores.py +83 -0
  204. vqlab/records/step_verdict.py +58 -0
  205. vqlab/runtime/CONTEXT.md +19 -0
  206. vqlab/runtime/__init__.py +1 -0
  207. vqlab/runtime/arch_resolve.py +295 -0
  208. vqlab/runtime/dense_shim.py +84 -0
  209. vqlab/runtime/equivalent/vq_dense.ac62e4e43f4a.py.txt +513 -0
  210. vqlab/runtime/equivalent/vq_switch.1534dd6b77b8.py.txt +4857 -0
  211. vqlab/runtime/equivalent/vq_switch.40870875499d.py.txt +4453 -0
  212. vqlab/runtime/equivalent/vq_switch.5abf28a7f096.py.txt +4958 -0
  213. vqlab/runtime/equivalent/vq_switch.6254c4531ed5.py.txt +5482 -0
  214. vqlab/runtime/equivalent/vq_switch.6d8ab6873adf.py.txt +4862 -0
  215. vqlab/runtime/equivalent/vq_switch.b41c1064c82b.py.txt +4856 -0
  216. vqlab/runtime/equivalent/vq_switch.e6fecc09a842.py.txt +4929 -0
  217. vqlab/runtime/equivalent_revisions.json +61 -0
  218. vqlab/runtime/gated_delta_chunked.py +153 -0
  219. vqlab/runtime/glm5_shim.py +171 -0
  220. vqlab/runtime/runtime_profile.py +193 -0
  221. vqlab/runtime/vq_dense.py +513 -0
  222. vqlab/runtime/vq_pack.py +129 -0
  223. vqlab/runtime/vq_switch.py +5495 -0
  224. vqlab/score/CONTEXT.md +31 -0
  225. vqlab/score/__init__.py +1 -0
  226. vqlab/score/act_stats.py +56 -0
  227. vqlab/score/kernel_truth.py +444 -0
  228. vqlab/score/kernel_truth_moe.py +129 -0
  229. vqlab/score/kl_damage.py +345 -0
  230. vqlab/score/kl_ladder.py +314 -0
  231. vqlab/score/kl_pair.py +133 -0
  232. vqlab/score/ppl_resident.py +80 -0
  233. vqlab/score/referee/referee_corpus.txt +350 -0
  234. vqlab/score/referee/referee_corpus_code_public.txt +1692 -0
  235. vqlab/score/referee/referee_corpus_code_public.txt.manifest.json +37 -0
  236. vqlab/score/referee/referee_corpus_literary.manifest.json +86 -0
  237. vqlab/score/referee/referee_corpus_literary.txt +22126 -0
  238. vqlab/score/referee/score_streaming.py +366 -0
  239. vqlab/score/runtime_equiv.py +179 -0
  240. vqlab/score/score_tasks_streaming.py +615 -0
  241. vqlab/score/stream_score.py +700 -0
  242. vqlab/selftest/CONTEXT.md +45 -0
  243. vqlab/selftest/__init__.py +141 -0
  244. vqlab/selftest/__main__.py +6 -0
  245. vqlab/selftest/build_checks.py +36 -0
  246. vqlab/selftest/family_checks.py +73 -0
  247. vqlab/selftest/fit_checks.py +121 -0
  248. vqlab/selftest/gate_checks.py +128 -0
  249. vqlab/selftest/kernel_checks.py +43 -0
  250. vqlab/selftest/layout_checks.py +90 -0
  251. vqlab/selftest/plan_checks.py +57 -0
  252. vqlab/selftest/queue_checks.py +106 -0
  253. vqlab/selftest/records_checks.py +171 -0
  254. vqlab/selftest/reselect_fixture.py +102 -0
  255. vqlab/selftest/skipzero_checks.py +32 -0
  256. vqlab/selftest/synthetic_model.py +136 -0
  257. vqlab/selftest/sz_bitexact_cpu.py +164 -0
  258. vqlab/selftest/sz_check_fixture.py +93 -0
  259. vqlab/ship/CONTEXT.md +27 -0
  260. vqlab/ship/__init__.py +1 -0
  261. vqlab/ship/card.py +338 -0
  262. vqlab/ship/card_checks.py +100 -0
  263. vqlab/ship/card_tables.py +115 -0
  264. vqlab/ship/mtp_run.py +55 -0
  265. vqlab/ship/publish.py +220 -0
  266. vqlab/ship/release_prep.py +139 -0
  267. vqlab/ship/serve.py +268 -0
  268. vqlab/ship/size.py +75 -0
  269. vqlab/skipzero/CONTEXT.md +88 -0
  270. vqlab/skipzero/__init__.py +2 -0
  271. vqlab/skipzero/skipzero_load.py +141 -0
  272. vqlab/skipzero/sz_bitexact.py +593 -0
  273. vqlab/skipzero/sz_check.py +162 -0
  274. vqlab/skipzero/sz_pack.py +355 -0
  275. vqlab/skipzero/sz_resident.py +756 -0
  276. vqlab/skipzero/sz_resident_build.py +175 -0
  277. vqlab/util/CONTEXT.md +22 -0
  278. vqlab/util/__init__.py +10 -0
  279. vqlab/util/clock.py +21 -0
  280. vqlab/util/errors.py +12 -0
  281. vqlab/util/fs.py +106 -0
  282. vqlab/util/geo.py +7 -0
  283. vqlab/util/hashing.py +73 -0
  284. vqlab/util/log.py +24 -0
  285. vqlab-0.1.0.dist-info/METADATA +346 -0
  286. vqlab-0.1.0.dist-info/RECORD +290 -0
  287. vqlab-0.1.0.dist-info/WHEEL +5 -0
  288. vqlab-0.1.0.dist-info/entry_points.txt +2 -0
  289. vqlab-0.1.0.dist-info/licenses/LICENSE +661 -0
  290. vqlab-0.1.0.dist-info/top_level.txt +1 -0
vqlab/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ """VQLab: size-targeted vector-quantized builds of large models on Apple
2
+ Silicon (MLX), and MTP speculative decoding over stock mlx-lm.
3
+
4
+ import vqlab
5
+ vqlab.use_knurlogic_arch() # deepseek_v4 / qwen4_exp / Knurlogic's edits
6
+ from mlx_lm import load
7
+ from vqlab import load_mtp_head, mtp_generate
8
+
9
+ model, tok = load(path, trust_remote_code=True)
10
+ head, _ = load_mtp_head(model, model_path=path)
11
+ print(mtp_generate(model, tok, "Explain VQ.", head, temp=0.7))
12
+
13
+ `import vqlab` changes nothing outside vqlab: the only bare import names
14
+ it answers for are the runtime stems (vq_switch, vq_dense, ...), aliases of
15
+ vqlab.runtime.<stem>. `use_knurlogic_arch()` is the
16
+ opt-in: it puts a finder on sys.meta_path so `mlx_lm.models.<name>` resolves
17
+ to Knurlogic's vendored architectures (needed to load deepseek_v4 and
18
+ qwen4_exp, which stock mlx-lm lacks). It is idempotent, and
19
+ VQLAB_VENDORED_ARCH=0 turns it off. Every `vqlab` CLI command calls it.
20
+
21
+ See README.md and METHODOLOGY.md.
22
+ """
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ from . import _layout # noqa: F401 stage folders + pre-split import names
27
+ from .core.box import arch_finder as _arch
28
+
29
+
30
+ def use_knurlogic_arch() -> bool:
31
+ """Serve Knurlogic's architectures (deepseek_v4, qwen4_exp, and its
32
+ qwen3_5 / gemma4 / glm5_next) as mlx_lm.models.<name>. Call it before
33
+ mlx_lm.load. Idempotent; returns False when VQLAB_VENDORED_ARCH=0 or
34
+ knurlogic is not installed (stock mlx-lm then serves every name)."""
35
+ return _arch.install_arch_finder()
36
+
37
+
38
+ def _main_is_vqlab() -> bool:
39
+ """True when the running program is one of vqlab's own scripts
40
+ (`python -m vqlab.<stage>.<tool>` or `python <pkg>/<stage>/<tool>.py`):
41
+ those are vqlab's entry points, and they load models as the CLI does."""
42
+ import os
43
+ import sys
44
+ argv = list(getattr(sys, "orig_argv", None) or [])
45
+ for i, a in enumerate(argv[1:], 1):
46
+ if a == "-m":
47
+ nxt = argv[i + 1] if i + 1 < len(argv) else ""
48
+ return nxt == "vqlab" or nxt.startswith("vqlab.")
49
+ if a.startswith("-m"):
50
+ return a[2:] == "vqlab" or a[2:].startswith("vqlab.")
51
+ if a.startswith("-"):
52
+ if a in ("-c",):
53
+ return False
54
+ continue
55
+ pkg = os.path.dirname(os.path.realpath(__file__))
56
+ return os.path.realpath(a).startswith(pkg + os.sep)
57
+ return False
58
+
59
+
60
+ if _main_is_vqlab():
61
+ use_knurlogic_arch()
62
+
63
+
64
+ # The MTP surface loads mlx, so it resolves on first use (PEP 562): a plain
65
+ # `import vqlab` -- and `import vqlab.api`, the stable library/MCP surface --
66
+ # stays free of mlx. `from vqlab import load_mtp_head` works as before.
67
+ _MTP_NAMES = ("FAMILIES", "FamilySpec", "MTPResponse", "load_mtp_head",
68
+ "mtp_generate", "mtp_stream_generate", "register")
69
+
70
+
71
+ def __getattr__(name):
72
+ if name in _MTP_NAMES:
73
+ from . import mtp
74
+ return getattr(mtp, name)
75
+ raise AttributeError(f"module 'vqlab' has no attribute {name!r}")
76
+
77
+
78
+ __all__ = ["use_knurlogic_arch", "FAMILIES", "FamilySpec", "MTPResponse", "load_mtp_head",
79
+ "mtp_generate", "mtp_stream_generate", "register", "__version__"]
vqlab/_layout.py ADDED
@@ -0,0 +1,225 @@
1
+ """Names shipped text depends on, and the locators that find a module as a FILE.
2
+
3
+ Every module has ONE canonical dotted name: its path under src/vqlab
4
+ (`vqlab.runtime.vq_switch`, `vqlab.fit.fit_moe`). PERMANENT lists the only
5
+ other spellings, kept forever because shipped text names them:
6
+
7
+ * every runtime/*.py stem, as `vqlab.<stem>` AND bare `<stem>`: the
8
+ SHIPPED runtime text (vq_dense.py's _resolve_kernel, dense_shim.py) and
9
+ published model.py files name them, and inside a bundle the files sit
10
+ flat beside model.py. Must stay byte-identical, so the names must stay;
11
+ * `vqlab.mtp_head*`, `vq_397b_codes` and `pack_artifact`: archived queue
12
+ files and ledgers name them.
13
+
14
+ Whatever name is used, the result is the SAME module object (_Alias): two
15
+ copies of vq_switch would mean two kernel caches and two sets of flag
16
+ globals.
17
+
18
+ Locators, for code that reads a module as a FILE rather than importing it:
19
+
20
+ find("smoke.py") -> path of any tool (and vq_397b_codes.py)
21
+ runtime_file("vq_switch.py") -> the repo runtime a bundle is spliced from
22
+ corpus("prose") -> a house referee corpus
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import importlib
27
+ import importlib.abc
28
+ import importlib.machinery
29
+ import importlib.util
30
+ import pathlib
31
+ import sys
32
+
33
+ PKG = pathlib.Path(__file__).resolve().parent
34
+ SRC = PKG.parent
35
+
36
+ PERMANENT = {
37
+ 'arch_resolve': 'vqlab.runtime.arch_resolve',
38
+ 'dense_shim': 'vqlab.runtime.dense_shim',
39
+ 'gated_delta_chunked': 'vqlab.runtime.gated_delta_chunked',
40
+ 'glm5_shim': 'vqlab.runtime.glm5_shim',
41
+ 'runtime_profile': 'vqlab.runtime.runtime_profile',
42
+ 'vq_dense': 'vqlab.runtime.vq_dense',
43
+ 'vq_pack': 'vqlab.runtime.vq_pack',
44
+ 'vq_switch': 'vqlab.runtime.vq_switch',
45
+ 'vqlab.arch_resolve': 'vqlab.runtime.arch_resolve',
46
+ 'vqlab.build.pack_artifact': 'vqlab.build.pack',
47
+ 'vqlab.dense_shim': 'vqlab.runtime.dense_shim',
48
+ 'vqlab.fit.vq_397b_codes': 'vqlab.fit.fit_moe',
49
+ 'vqlab.gated_delta_chunked': 'vqlab.runtime.gated_delta_chunked',
50
+ 'vqlab.glm5_shim': 'vqlab.runtime.glm5_shim',
51
+ 'vqlab.mtp_head': 'vqlab.mtp.mtp_head',
52
+ 'vqlab.mtp_head_ds4': 'vqlab.build.mtp_head_ds4',
53
+ 'vqlab.mtp_head_glm5': 'vqlab.mtp.mtp_head_glm5',
54
+ 'vqlab.mtp_head_qwen35': 'vqlab.mtp.mtp_head_qwen35',
55
+ 'vqlab.pack_artifact': 'vqlab.build.pack',
56
+ 'vqlab.runtime_profile': 'vqlab.runtime.runtime_profile',
57
+ 'vqlab.vq_397b_codes': 'vqlab.fit.fit_moe',
58
+ 'vqlab.vq_dense': 'vqlab.runtime.vq_dense',
59
+ 'vqlab.vq_pack': 'vqlab.runtime.vq_pack',
60
+ 'vqlab.vq_switch': 'vqlab.runtime.vq_switch',
61
+ }
62
+
63
+ assert not {"queue", "vqlab.queue"} & set(PERMANENT) # stdlib `queue` stays the stdlib's
64
+
65
+ # Old FILENAMES the PERMANENT names imply (vq_397b_codes.py -> fit_moe.py,
66
+ # pack_artifact.py -> pack.py): archived queue files and ledgers name them.
67
+ FILE_NAMES = {k.rpartition(".")[2]: v.rpartition(".")[2] for k, v in PERMANENT.items()
68
+ if k.rpartition(".")[2] != v.rpartition(".")[2]}
69
+
70
+
71
+ # Folders find() searches, in order: a current filename needs no table.
72
+ _FIND_DIRS = ("", "runtime", "core", "core/artifacts", "core/source", "core/box",
73
+ "family", "plan", "fit", "build", "bundle",
74
+ "gate", "score", "bench", "records", "ship", "agents", "mtp",
75
+ "queue", "selftest", "skipzero", "ops", "util", "score/referee")
76
+
77
+
78
+ def find(name: str) -> pathlib.Path:
79
+ """Path of a module FILE by its filename or relative path. The old
80
+ filenames PERMANENT implies (vq_397b_codes.py) map to their module."""
81
+ stem, dot, ext = name.rpartition("/")[2].partition(".")
82
+ new = FILE_NAMES.get(stem)
83
+ if new and ext == "py":
84
+ name = name[:len(name) - len(stem + dot + ext)] + new + dot + ext
85
+ for d in _FIND_DIRS:
86
+ p = PKG / d / name
87
+ if p.exists():
88
+ return p
89
+ if name.startswith("referee/"): # old relative spelling
90
+ return find(name.split("/", 1)[1])
91
+ raise FileNotFoundError(f"vqlab: no module file {name!r} under {PKG}")
92
+
93
+
94
+ def runtime_file(name: str) -> pathlib.Path:
95
+ """The repo's copy of a shipped runtime file (vq_switch.py, vq_dense.py,
96
+ vq_pack.py, dense_shim.py, glm5_shim.py). This is the text bundlers
97
+ splice and check-bundle compares against; its flag DEFAULTS are the
98
+ repo's, so a caller that needs a specific profile applies it with
99
+ runtime_profile.apply_profile / resolve_runtime."""
100
+ p = PKG / "runtime" / name
101
+ if not p.exists():
102
+ raise FileNotFoundError(f"vqlab: {name} is not a runtime file ({p})")
103
+ return p
104
+
105
+
106
+ CORPORA = {"prose": "referee_corpus.txt",
107
+ "code": "referee_corpus_code_public.txt",
108
+ "lit": "referee_corpus_literary.txt"}
109
+ _CORPUS_ALIAS = {"wikitext": "prose", "code-public": "code", "literary": "lit"}
110
+
111
+
112
+ def corpus(name: str = "prose") -> pathlib.Path:
113
+ """A house referee corpus by NAME (prose / code / lit), never by a path
114
+ built from __file__: a scratch script that hardcodes the repo layout
115
+ breaks silently when the layout moves (night 4, 2026-09-26). A path
116
+ argument is passed through legacy_path, so old spellings still resolve."""
117
+ key = _CORPUS_ALIAS.get(name, name)
118
+ if key in CORPORA:
119
+ p = PKG / "score" / "referee" / CORPORA[key]
120
+ else:
121
+ p = pathlib.Path(legacy_path(str(name)))
122
+ if not p.is_file():
123
+ raise FileNotFoundError(f"vqlab: no corpus {name!r} (have {sorted(CORPORA)})")
124
+ return p
125
+
126
+
127
+ def legacy_path(p):
128
+ """Map a path recorded BEFORE the stage split onto its new home.
129
+
130
+ Teacher caches store their corpus path in meta.json (absolute or
131
+ repo-relative) and shell chains pass corpora by path; both still name
132
+ src/vqlab/referee/. Existing paths are returned unchanged."""
133
+ s = str(p)
134
+ if pathlib.Path(s).exists():
135
+ return p
136
+ # "vqlab/referee/" (no src/) also covers an INSTALLED package's site-packages path
137
+ for old, new in (("vqlab/referee/", "vqlab/score/referee/"),):
138
+ if old in s:
139
+ cand = s.replace(old, new)
140
+ if pathlib.Path(cand).exists() or not pathlib.Path(s).is_absolute():
141
+ return type(p)(cand) if not isinstance(p, str) else cand
142
+ return p
143
+
144
+
145
+ class _Alias(importlib.abc.MetaPathFinder, importlib.abc.Loader):
146
+ """ONE module object per module, whatever name it is imported by.
147
+
148
+ Without this, `import vq_switch` and `vqlab.runtime.vq_switch` load the
149
+ file twice: two kernel caches, two sets of env-flag globals, and a test
150
+ that reloads one name silently leaves the other untouched.
151
+
152
+ It answers ONLY for the PERMANENT names: `vqlab.*` spellings (our
153
+ namespace) and the bare runtime stems a bundle's runtime text refers to
154
+ (vq_switch, vq_dense, ...). Every other name gets None from it, so it
155
+ cannot shadow another package's module.
156
+
157
+ It sits FIRST on sys.meta_path, and that is required, not habit: a
158
+ script directory holding a copy of vq_switch.py (sys.path[0]) would
159
+ otherwise give the runtime a second module object.
160
+ """
161
+
162
+ def __init__(self):
163
+ self.map = PERMANENT
164
+ self._fresh = set() # ids of modules create_module just imported
165
+
166
+ def _canon(self, fullname):
167
+ return self.map.get(fullname)
168
+
169
+ def find_spec(self, fullname, path=None, target=None):
170
+ new = self._canon(fullname)
171
+ if new is None or new == fullname:
172
+ return None
173
+ t = importlib.util.find_spec(new)
174
+ return importlib.util.spec_from_loader(
175
+ fullname, self, origin=t.origin if t else f"alias of {new}")
176
+
177
+ def create_module(self, spec):
178
+ mod = importlib.import_module(self._canon(spec.name))
179
+ sys.modules[spec.name] = mod
180
+ self._fresh.add(id(mod))
181
+ return mod
182
+
183
+ def exec_module(self, module):
184
+ """First import: the canonical import already executed it. RELOAD
185
+ (importlib.reload by the alias name, which is how the flag tests
186
+ re-read env defaults) must genuinely re-execute. Either way the
187
+ module keeps its CANONICAL identity; the import system just stamped
188
+ the alias spec on it."""
189
+ name = module.__spec__.name
190
+ canon = self._canon(name) or name
191
+ # Ask the path finder for the FILE's spec. importlib.util.find_spec
192
+ # would return the module's current __spec__ -- the alias spec the
193
+ # import system just stamped on it -- and recurse back into us.
194
+ parent, _, _ = canon.rpartition(".")
195
+ spec = importlib.machinery.PathFinder.find_spec(
196
+ canon, importlib.import_module(parent).__path__)
197
+ if id(module) in self._fresh:
198
+ self._fresh.discard(id(module))
199
+ else:
200
+ spec.loader.exec_module(module)
201
+ module.__spec__, module.__name__, module.__loader__ = (
202
+ spec, spec.name, spec.loader)
203
+
204
+ # `python -m vqlab.<old name>` goes through runpy, which asks the loader
205
+ # for code rather than a module. Hand it the real module's code.
206
+ def _target(self, fullname):
207
+ return importlib.util.find_spec(self._canon(fullname))
208
+
209
+ def get_code(self, fullname):
210
+ t = self._target(fullname)
211
+ return t.loader.get_code(t.name)
212
+
213
+ def get_source(self, fullname):
214
+ t = self._target(fullname)
215
+ return t.loader.get_source(t.name)
216
+
217
+ def is_package(self, fullname):
218
+ return False
219
+
220
+
221
+ def install_import_aliases():
222
+ if not any(isinstance(f, _Alias) for f in sys.meta_path):
223
+ sys.meta_path.insert(0, _Alias()) # first: see _Alias docstring
224
+
225
+ install_import_aliases()
@@ -0,0 +1,50 @@
1
+ # agents/ — Agents: the lab over MCP
2
+
3
+ ## Inputs
4
+ JSON-RPC over stdio (`mcp`). (Queue files: `queue/`, untangle step 12.)
5
+
6
+ ## Process
7
+ `vqlab mcp`: `where_is`, `list_artifacts`, `run` (allowlisted, detached, under the GPU lease), `next_f_number` (reserve=true before a long run), `findings_append`, ...
8
+
9
+ The server is the package `mcp/` (untangle step 13), and since step 16b it is a THIN ADAPTER: each tool is a JSON schema plus an argument mapping onto `vqlab.api` (or `vqlab.ops` where the api deliberately has no call: `run` returns the launcher's raw record, `artifact_config`, `read_doc`, `queue_status`). No lab logic lives here; the implementations are in `src/vqlab/ops/` (see its CONTEXT.md). `mcp/server.py` (stdio JSON-RPC loop + the tool table: name -> fn, schema; `--supervise` delegates to `vqlab.ops.supervisor`), `mcp/common.py` (`ToolError` = `vqlab.ops.errors.LabError` = `api.ApiError`), `mcp/tools_storage.py`, `mcp/tools_runs.py`, `mcp/tools_findings.py` (the `t_*` adapters). Patch and import the defining `vqlab.ops.*` module, never an adapter.
10
+
11
+ `vqlab lab scratch reclaimable` (`scratch.py`) surveys the scratch root, or each `--root` (HDD folders too), by evidence class. `packed`: an unpacked dir whose verified pack's build record names it. `preflight`: `<preflight_dir>/<queue>` copies whose queue (state.json, read through `queue.report.live_status` / `runner_pid`) is finished with no live runner, or whose queue record is gone. `fits`: dirs whose every safetensors holds only VQ triples already filed in the fit store (fit_id + size from `fit_store.read_index`, then tensor bytes against the filed copy; `--full` re-hashes the filed copy). `caches`: unregistered KL caches sha256-identical to a registered one (`registry.teacher_caches`); a registered cache is always KEPT. `campaigns`: `F###-name` folders whose finding is in the log, ARCHIVE candidates only. Each item carries its evidence (`--json`: `cls`, `evidence`, `detail`); every other top-level dir is reported KEPT with the reason; anything a symlink (roots + models) resolves into is kept. It prints the rm lines and never deletes. `reclaimable()` is the `packed` class alone (the GUI's).
12
+
13
+ `vqlab queue run <file>` (code in `queue/`, see its CONTEXT.md): a multi-step campaign (a night of scoring) run from a git worktree PINNED at one commit, holding the same GPU lease as `run`, retrying resumable builds, refusing unsmoked pins, and failing a step on a nonzero exit, a traceback, or missing output. `--preflight` runs every step's small real version first; `--detach` survives the shell. `queue status` (and MCP `queue_status`) prints an ETA from the running step's own progress lines (fit-moe `[i/N] shard (Ts)`, kl-ladder cells) or past passed runs of the same step, else "ETA unknown". `--on BOX` first checks over ssh that every path the queue names exists AND reads on that box (an SMB mount can list while reads fail EIO), and refuses with the fix ("remount <volume> on <box>").
14
+
15
+ `vqlab reserve` (`queue/reserve.py`) `BOX|here --for WHO --until +3h|HH:MM [--note]`, `--list`, `--release`: intent, not a lock. Stored in `<scratch>/vqlab-reservations.json` (the shared SSD every box mounts; `$VQLAB_RESERVATIONS` overrides; falls back to `~/.vqlab/reservations.json`, local-only, when scratch is unreachable). Expired ones are ignored. MCP `gpu_state` lists every box's; `queue run` / `queue run --on BOX` refuse a box reserved for someone other than `$VQLAB_WHO` (else login) unless `--override-reservation`, which state.json records.
16
+
17
+ `[boxes.NAME] teachers = "<path>"`: that box's local teacher copies. `config.teachers()` returns it when running ON that box (`$VQLAB_BOX`, which `--on` sets, else hostname), and `vqlab <cmd>` expands a `<teachers>/X` argument through it, so a queue step naming `<teachers>/Model` reads the local copy there.
18
+
19
+ `vqlab lab config` / `lab config set KEY PATH` / `lab config init` and the GUI's Settings view share ONE implementation, `agents/settings.py` (`locations`, `validate`, `set_value`, `campaign_sizes`): validate (absolute, a directory or creatable with `--create`, writable, free space shown), then rewrite only that key's line in `[paths]`. Every location defaults under the lab folder `lab` (`$VQLAB_HOME`, `[paths] lab`, else `~/VQLab`): `<lab>/models`, `scratch`, `fits`, `records`, and `teachers` (bf16 teachers) defaults to the models folder; with no `lab` set, data already under `~/.vqlab` or `~/.local/share/vqlab` keeps resolving there. `usage()` (GUI `/api/storage/usage`) groups the locations by volume with what each holds. `records` (`$VQLAB_RECORDS`, else `<lab>/records`) holds user-written state: `FINDINGS-LOG.md` (`$VQLAB_FINDINGS_LOG` still overrides), `registry/`, `families/` (`$VQLAB_FAMILIES_DIR`), `research/`, `queues/` (`$VQLAB_QUEUE_DIR`). Accessors: `config.records()`, `findings_log()`, `registry_dir()`, `families_dir()`, `research_dir()`, `queues_dir()`. A checkout that wants the old in-repo `lab/` sets `records` to it.
20
+
21
+ ## Outputs
22
+ Detached runs under the MCP runs dir, FINDINGS-LOG entries; queues under `<records>/queues/<stamp>-<name>/` (state.json, steps/NN-name/{cmd,stdout,stderr,verdict.json}).
23
+
24
+ ## Rules that bite
25
+ - Use `where_is` before claiming anything is missing.
26
+ - `run` launches through `python -m vqlab.cli`, so every agent job is in the run log.
27
+ - A queue step never imports the live checkout: commit mid-run freely. Uncommitted src/ changes are NOT in the queue (it refuses unless --allow-dirty).
28
+ - Every queue step needs a `preflight` block; run `--preflight` before any multi-hour queue.
29
+ - `onboard.py` (`vqlab plan onboard`) lives here, not in plan/: it drives the MCP server's `t_run`/`t_status`. The GPU lease file logic is `core/box/gpu_lease.py`.
30
+
31
+ ## Files
32
+ - `config_cmd.py`: `vqlab lab config`: show, set or initialise storage locations (over settings.py)
33
+ - `doctor.py`: `vqlab lab doctor`: which interpreter, packages, storage and credentials a run would use, and whether a newer vqlab is out
34
+ - `updates.py`: is a newer vqlab on PyPI (one request a day at most, cached in records; VQLAB_OFFLINE skips it); doctor and the GUI read it
35
+ - `bug_report.py`: `vqlab lab report-bug`: a prefilled GitHub issue (versions, chip, last failed command; portable paths, no argv); prints the link, never sends
36
+ - `model_scan.py`: `vqlab lab scan`: models already on this machine (vqlab's own roots, the Hugging Face cache, LM Studio, exo, Ollama, `[scan] extra`), tagged original / quantized / incomplete / unreadable with their vqlab family, into `<records>/registry/found.jsonl`; reads config.json and file lists only. The GUI rescans at launch and on Rescan; first run scans once
37
+ - `first_run.py`: on a new install (no config, no location env, no older data) the first `vqlab` command creates `~/VQLab/{models,scratch,fits,records}` (bf16 teachers default to models/) and a config naming it, and says so once; `$VQLAB_NO_SETUP=1` skips it
38
+ - `downloads.py`: the GUI's Hub downloads (`/api/downloads`, POST `/start` `/stop`): one fetch process per repo (`_fetch.py`: `snapshot_download` into `<teachers>/<org>--<name>`), progress from bytes on disk, a model scan when done; never deletes
39
+ - `gui.py`: `vqlab lab gui`: serves the VQ Lab GUI (Library · Recipe · Jobs · Downloads · Storage: gui_static/index.html, built from gui_static/bench/bench.html) and the JSON it reads
40
+ - `jobs.py`: the GUI's Start / Stop / Resume of `vqlab queue` runs: a confirmed, single-use plan token, then the queue CLI (never a step itself)
41
+ - `teachers.py`: the GUI's teachers (`/api/teachers`): profiled teachers with exact per-geometry sizes, stored fits and whether a build chain exists, plus scanned originals that only need `family-profile` (POST `/api/teachers/profile`)
42
+ - `gui_static/bench/make_jobs_snapshot.py`: regenerates the GUI's embedded jobs snapshot (bench.html / index.html)
43
+ - `mcp/server.py`: `vqlab lab mcp`: the stdio JSON-RPC loop and the tool table (name -> fn, schema)
44
+ - `mcp/common.py`: `ToolError` (= `vqlab.ops.errors.LabError`)
45
+ - `mcp/tools_storage.py`: adapters: where_is, list_artifacts, artifact_config, read_doc, disk_free
46
+ - `mcp/tools_runs.py`: adapters: run, status, stop, list_runs, gpu_state, queue_status
47
+ - `mcp/tools_findings.py`: adapters: next_f_number (reserve / release), findings_tail, findings_append
48
+ - `onboard.py`: `vqlab plan onboard`: sequence a new teacher through docs/ONBOARDING.md
49
+ - `scratch.py`: `vqlab lab scratch`: what on scratch (or any root) a human could delete or archive, by evidence class, and why
50
+ - `settings.py`: storage settings: read, validate and write vqlab's locations (CLI and GUI share it)
@@ -0,0 +1 @@
1
+ """vqlab agents stage: see CONTEXT.md in this folder."""
vqlab/agents/_fetch.py ADDED
@@ -0,0 +1,46 @@
1
+ """One GUI download's own process (agents/downloads.py starts it):
2
+
3
+ python -m vqlab.agents._fetch REPO DEST LOCKFILE RCFILE PATTERN...
4
+
5
+ `huggingface_hub.snapshot_download` of REPO into DEST, only the files the
6
+ PATTERNs allow. The process holds a flock on DEST/LOCKFILE for its whole
7
+ life, with its pid in it: the server signals a recorded pid only while that
8
+ lock says it is still this fetch (a pid alone can be reused after a
9
+ restart). A second fetch of the same folder exits 2 and touches nothing. It
10
+ records how it ended in DEST/RCFILE: 0 done, 1 failed (its stderr log says
11
+ why). A stopped or interrupted fetch (SIGTERM, ctrl-c) records nothing.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import fcntl
16
+ import os
17
+ import pathlib
18
+ import sys
19
+
20
+ from huggingface_hub import snapshot_download
21
+
22
+
23
+ def main(argv: list[str]) -> int:
24
+ repo, dest, lockfile, rcfile, *allow = argv
25
+ d = pathlib.Path(dest)
26
+ lk = open(d / lockfile, "a+") # held open until exit: the lock is the liveness
27
+ try:
28
+ fcntl.flock(lk, fcntl.LOCK_EX | fcntl.LOCK_NB)
29
+ except BlockingIOError:
30
+ # another fetch owns this folder: its pid and exit file stay its own
31
+ print(f"another fetch of {d} is running; not starting a second", file=sys.stderr)
32
+ return 2
33
+ lk.truncate(0)
34
+ lk.write(str(os.getpid()))
35
+ lk.flush()
36
+ try:
37
+ snapshot_download(repo, local_dir=d, allow_patterns=allow)
38
+ except Exception:
39
+ (d / rcfile).write_text("1")
40
+ raise
41
+ (d / rcfile).write_text("0")
42
+ return 0
43
+
44
+
45
+ if __name__ == "__main__":
46
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,107 @@
1
+ """`vqlab lab report-bug`: a GitHub issue, filled in with what a bug needs.
2
+
3
+ Builds the issue body from the versions that decide behaviour (vqlab,
4
+ Python, macOS, the chip and its memory, mlx, mlx-lm, knurlogic,
5
+ huggingface_hub) and the last command that failed, then prints the
6
+ new-issue link (and, with --open, opens it). Nothing is sent: the browser
7
+ shows the issue for the user to read, edit and submit. Every path in it is
8
+ written in its portable form (config.portable_text), and argv is left out:
9
+ it names the user's files.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import platform
15
+ import sys
16
+ import urllib.parse
17
+
18
+ from vqlab import config
19
+
20
+ PACKAGES = ("mlx", "mlx-lm", "knurlogic", "huggingface_hub")
21
+ MAX_URL = 7000 # GitHub refuses longer new-issue links
22
+
23
+
24
+ def issues_url() -> str:
25
+ """The project's issue tracker, from its metadata (pyproject
26
+ [project.urls] Issues): the installed package's, else the checkout's."""
27
+ from importlib.metadata import PackageNotFoundError, metadata
28
+ try:
29
+ for u in metadata("vqlab").get_all("Project-URL") or []:
30
+ name, _, url = u.partition(", ")
31
+ if name == "Issues":
32
+ return url
33
+ except PackageNotFoundError:
34
+ pass
35
+ import tomllib
36
+
37
+ from vqlab import _layout
38
+ pp = _layout.SRC.parent / "pyproject.toml"
39
+ return tomllib.loads(pp.read_text())["project"]["urls"]["Issues"]
40
+
41
+
42
+ def environment() -> list[str]:
43
+ """`name: version` lines for what decides behaviour; no paths."""
44
+ import vqlab
45
+ from vqlab.agents import doctor
46
+ lines = [f"vqlab: {vqlab.__version__}", f"python: {platform.python_version()}",
47
+ f"macOS: {platform.mac_ver()[0] or platform.platform()}"]
48
+ m = doctor.metal_info()
49
+ lines.append(f"chip: {m.get('device')}, {m.get('memory_gib')} GiB" if m.get("available")
50
+ else "chip: Metal unavailable")
51
+ lines += [f"{p}: {doctor.package_version(p) or 'not installed'}" for p in PACKAGES]
52
+ return lines
53
+
54
+
55
+ def last_failure() -> dict | None:
56
+ """The most recent logged command that ended with an error: its name,
57
+ exit status and error text (portable), never its arguments."""
58
+ from vqlab.records import runlog
59
+ for r in reversed(runlog.runs()):
60
+ rc = (r.get("end") or {}).get("rc")
61
+ if rc not in (None, 0):
62
+ return {"cmd": r.get("cmd"), "time": r.get("time"),
63
+ "rc": config.portable_text(str(rc))}
64
+ return None
65
+
66
+
67
+ def body(what: str = "") -> str:
68
+ """The issue text: the user's description, then the facts."""
69
+ out = ["**What happened, and what did you expect?**", "", what or "<describe it here>", "",
70
+ "**Command and output** (paste them; check for anything private first)", "",
71
+ "```", "", "```", "", "**Environment**", "", "```", *environment(), "```"]
72
+ fail = last_failure()
73
+ if fail:
74
+ out += ["", "**Last failed command**", "", "```",
75
+ f"vqlab {fail['cmd']} ({fail['time']})", f"exit: {fail['rc']}", "```"]
76
+ return "\n".join(out) + "\n"
77
+
78
+
79
+ def new_issue_url(title: str, text: str) -> str:
80
+ """GitHub's prefilled new-issue link; the body is cut to fit the limit."""
81
+ base = issues_url().rstrip("/") + "/new?"
82
+ q = {"title": title, "labels": "bug", "body": text}
83
+ url = base + urllib.parse.urlencode(q)
84
+ while len(url) > MAX_URL and len(q["body"]) > 200:
85
+ q["body"] = q["body"][: len(q["body"]) * 3 // 4] + "\n…(cut to fit the link)\n"
86
+ url = base + urllib.parse.urlencode(q)
87
+ return url
88
+
89
+
90
+ def main(argv=None) -> int:
91
+ ap = argparse.ArgumentParser(prog="vqlab lab report-bug", description=__doc__.split("\n")[0])
92
+ ap.add_argument("--title", default="", help="one line saying what went wrong")
93
+ ap.add_argument("--what", default="", help="what happened and what you expected")
94
+ ap.add_argument("--open", action="store_true", help="open the issue in the browser")
95
+ a = ap.parse_args(argv)
96
+ text = body(a.what)
97
+ url = new_issue_url(a.title or "bug: ", text)
98
+ print(text)
99
+ print(f"Open this to file it on GitHub (nothing is sent until you submit):\n{url}")
100
+ if a.open:
101
+ import webbrowser
102
+ webbrowser.open(url)
103
+ return 0
104
+
105
+
106
+ if __name__ == "__main__":
107
+ sys.exit(main())