blitz-cli 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/PKG-INFO +6 -11
- blitz_cli-0.4.0/README.md +22 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/__init__.py +7 -6
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_client.py +16 -28
- blitz_cli-0.4.0/blitz_cli/_scaffold.py +103 -0
- blitz_cli-0.4.0/blitz_cli/_skill.py +59 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/cli.py +140 -36
- blitz_cli-0.4.0/blitz_cli/skills/configure-training/SKILL.md +166 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/skills/onboard-to-blitz/SKILL.md +8 -2
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/Dockerfile.tmpl +1 -2
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/Makefile.tmpl +5 -7
- blitz_cli-0.4.0/blitz_cli/templates/README.md.tmpl +38 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/dockerignore.tmpl +2 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/requirements.txt.tmpl +1 -1
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/train.py.tmpl +46 -14
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/pyproject.toml +1 -1
- blitz_cli-0.4.0/tests/test_scaffold.py +89 -0
- blitz_cli-0.3.0/README.md +0 -27
- blitz_cli-0.3.0/blitz_cli/_scaffold.py +0 -111
- blitz_cli-0.3.0/blitz_cli/_skill.py +0 -42
- blitz_cli-0.3.0/blitz_cli/templates/README.md.tmpl +0 -36
- blitz_cli-0.3.0/blitz_cli/templates/eval.py.tmpl +0 -105
- blitz_cli-0.3.0/tests/test_scaffold.py +0 -80
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/.github/workflows/homebrew.yml +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/.gitignore +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/LICENSE +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/__init__.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_classify.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_js.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_models.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_python.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_report.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_walk.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/skills/__init__.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/__init__.py +0 -0
- {blitz_cli-0.3.0 → blitz_cli-0.4.0}/tests/test_scan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blitz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Developer CLI for Blitz: pull a workflow's training data + base-model recommendation and scaffold a runnable QLoRA training container.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -10,9 +10,8 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
# blitz-cli
|
|
11
11
|
|
|
12
12
|
Developer CLI for [Blitz](https://github.com/sparepartslabs/blitz-sdk-py). Pull a
|
|
13
|
-
workflow's captured traces as a fine-tuning dataset
|
|
14
|
-
|
|
15
|
-
close the loop by grading the trained student against the held-out eval set.
|
|
13
|
+
workflow's captured traces as a fine-tuning dataset, and scaffold a
|
|
14
|
+
self-contained, runnable QLoRA training container.
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
17
|
pip install blitz-cli
|
|
@@ -21,16 +20,12 @@ export BLITZ_API_KEY=blz_... # a read-scoped project key (mint one in the dash
|
|
|
21
20
|
blitz scaffold -p proj_abc -w mechanic-assistant -o ./train
|
|
22
21
|
|
|
23
22
|
cd train
|
|
24
|
-
make build && make train
|
|
23
|
+
make build && make train
|
|
25
24
|
```
|
|
26
25
|
|
|
27
|
-
`blitz scaffold` writes `./train` with `data/dataset.jsonl
|
|
28
|
-
`
|
|
29
|
-
`train.py` + `eval.py` + `Makefile`. Bring your own NVIDIA GPU.
|
|
26
|
+
`blitz scaffold` writes `./train` with `data/dataset.jsonl` and a `Dockerfile` +
|
|
27
|
+
`train.py` + `Makefile`. Bring your own NVIDIA GPU.
|
|
30
28
|
|
|
31
29
|
This package is intentionally dependency-free (stdlib + the Blitz HTTP API). The
|
|
32
30
|
heavy ML stack (torch / transformers / trl / peft) is pinned only in the
|
|
33
31
|
*generated* project's `requirements.txt`, installed inside the training image.
|
|
34
|
-
|
|
35
|
-
The complementary tracing SDK (`pip install blitz-sdk`, `import blitz`) lives in
|
|
36
|
-
[`../blitz-sdk`](../blitz-sdk).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# blitz-cli
|
|
2
|
+
|
|
3
|
+
Developer CLI for [Blitz](https://github.com/sparepartslabs/blitz-sdk-py). Pull a
|
|
4
|
+
workflow's captured traces as a fine-tuning dataset, and scaffold a
|
|
5
|
+
self-contained, runnable QLoRA training container.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install blitz-cli
|
|
9
|
+
|
|
10
|
+
export BLITZ_API_KEY=blz_... # a read-scoped project key (mint one in the dashboard)
|
|
11
|
+
blitz scaffold -p proj_abc -w mechanic-assistant -o ./train
|
|
12
|
+
|
|
13
|
+
cd train
|
|
14
|
+
make build && make train
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`blitz scaffold` writes `./train` with `data/dataset.jsonl` and a `Dockerfile` +
|
|
18
|
+
`train.py` + `Makefile`. Bring your own NVIDIA GPU.
|
|
19
|
+
|
|
20
|
+
This package is intentionally dependency-free (stdlib + the Blitz HTTP API). The
|
|
21
|
+
heavy ML stack (torch / transformers / trl / peft) is pinned only in the
|
|
22
|
+
*generated* project's `requirements.txt`, installed inside the training image.
|
|
@@ -12,15 +12,16 @@ Commands:
|
|
|
12
12
|
- ``blitz verify [--wait]`` — confirm sample traces have landed in Blitz,
|
|
13
13
|
authenticated by the ingest key (``BLITZ_API_KEY``).
|
|
14
14
|
|
|
15
|
-
- ``blitz skill install`` — drop the ``onboard-to-blitz
|
|
16
|
-
the repo's ``.claude/skills`` so Claude can run the
|
|
15
|
+
- ``blitz skill install`` — drop the Blitz Claude Code skills (``onboard-to-blitz``,
|
|
16
|
+
``configure-training``) into the repo's ``.claude/skills`` so Claude can run the
|
|
17
|
+
onboarding and training playbooks.
|
|
17
18
|
|
|
18
19
|
- ``blitz scaffold -p <project> -w <workflow> -o ./train`` — download a
|
|
19
|
-
workflow's SFT dataset,
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
workflow's SFT dataset, then emit a self-contained, runnable training project
|
|
21
|
+
(Dockerfile + train.py). Authenticates with a read-scoped Blitz API key
|
|
22
|
+
(``BLITZ_API_KEY``).
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
25
|
__all__ = ["__version__"]
|
|
25
26
|
|
|
26
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.4.0"
|
|
@@ -92,30 +92,34 @@ class BlitzClient:
|
|
|
92
92
|
if line:
|
|
93
93
|
yield json.loads(line)
|
|
94
94
|
|
|
95
|
-
def
|
|
95
|
+
def _send_json(self, path: str, body: dict, method: str) -> dict:
|
|
96
96
|
data = json.dumps(body).encode("utf-8")
|
|
97
97
|
req = urllib.request.Request(
|
|
98
98
|
self._url(path),
|
|
99
99
|
data=data,
|
|
100
|
-
method=
|
|
100
|
+
method=method,
|
|
101
101
|
headers={**self._headers, "content-type": "application/json"},
|
|
102
102
|
)
|
|
103
103
|
with self._open(req) as resp:
|
|
104
104
|
return json.loads(resp.read().decode("utf-8"))
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
def _post_json(self, path: str, body: dict) -> dict:
|
|
107
|
+
return self._send_json(path, body, "POST")
|
|
108
|
+
|
|
109
|
+
def _put_json(self, path: str, body: dict) -> dict:
|
|
110
|
+
return self._send_json(path, body, "PUT")
|
|
111
|
+
|
|
112
|
+
# -- public API (one method per endpoint the CLI needs) -----------------
|
|
107
113
|
|
|
108
114
|
def _p(self, suffix: str) -> str:
|
|
109
115
|
return f"/blitz/projects/{self._project}{suffix}"
|
|
110
116
|
|
|
111
|
-
def
|
|
112
|
-
return self._get_json(self._p("/
|
|
117
|
+
def get_training_config(self, workflow: str) -> dict:
|
|
118
|
+
return self._get_json(self._p("/training-config"), {"workflow": workflow})
|
|
113
119
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def eval_summary(self, workflow: str) -> dict:
|
|
118
|
-
return self._get_json(self._p("/eval-set/summary"), {"workflow": workflow})
|
|
120
|
+
def set_training_config(self, config: dict) -> dict:
|
|
121
|
+
# config is the full flat config (includes `workflow`); the server keys on it.
|
|
122
|
+
return self._put_json(self._p("/training-config"), config)
|
|
119
123
|
|
|
120
124
|
def download_dataset(
|
|
121
125
|
self, workflow: Optional[str] = None, include_synthetic: bool = True
|
|
@@ -125,24 +129,8 @@ class BlitzClient:
|
|
|
125
129
|
{"workflow": workflow, "include_synthetic": include_synthetic},
|
|
126
130
|
)
|
|
127
131
|
|
|
128
|
-
def
|
|
129
|
-
return self.
|
|
130
|
-
|
|
131
|
-
def submit_eval(
|
|
132
|
-
self, workflow: str, predictions: list, grader: str = "auto"
|
|
133
|
-
) -> dict:
|
|
134
|
-
return self._post_json(
|
|
135
|
-
self._p("/eval"),
|
|
136
|
-
{
|
|
137
|
-
"workflow": workflow,
|
|
138
|
-
"candidate": "supplied",
|
|
139
|
-
"grader": grader,
|
|
140
|
-
"predictions": predictions,
|
|
141
|
-
},
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
def get_eval_run(self, run_id: str) -> dict:
|
|
145
|
-
return self._get_json(self._p(f"/eval/runs/{run_id}"))
|
|
132
|
+
def training_readiness(self, workflow: str) -> dict:
|
|
133
|
+
return self._get_json(self._p("/training-readiness"), {"workflow": workflow})
|
|
146
134
|
|
|
147
135
|
def ingest_status(self, workflow: Optional[str] = None) -> dict:
|
|
148
136
|
"""Trace summary for `blitz verify`, authed by the ingest key. This is
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Render a runnable QLoRA training project + derive a training config.
|
|
2
|
+
|
|
3
|
+
Templates live in ``blitz_cli/templates`` and ship as package data. ``train.py``
|
|
4
|
+
and ``requirements.txt`` are copied verbatim (they read ``config.json`` at
|
|
5
|
+
runtime, so they need no substitution and stay valid Python regardless of the
|
|
6
|
+
chosen base model); ``Dockerfile``/``Makefile``/``README`` get ``$var`` substitution
|
|
7
|
+
via string.Template (avoids the brace-escaping pain of str.format on code).
|
|
8
|
+
|
|
9
|
+
The config itself is no longer written into the project at scaffold time. The
|
|
10
|
+
onboarding skill assembles it (via ``build_config``) and pushes it to Blitz with
|
|
11
|
+
``blitz config set``; the training container fetches it at run start and writes its
|
|
12
|
+
own ``config.json``. ``build_config`` stays here as the shared deriver that turns a
|
|
13
|
+
base-model choice into a full config with sensible, size-scaled hyperparameters.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from importlib import resources
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from string import Template
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clamp_seq_len(raw: int) -> int:
|
|
24
|
+
"""Clamp a raw sequence length (p95_input + max_output) to the trainer's
|
|
25
|
+
supported range. Defaults to 2048 when nothing usable is supplied."""
|
|
26
|
+
return max(1024, min(8192, int(raw or 0) or 2048))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_config(
|
|
30
|
+
workflow: str,
|
|
31
|
+
base_model_hf: str,
|
|
32
|
+
params_b: float = 7.0,
|
|
33
|
+
license: str = "",
|
|
34
|
+
seq_len: int = 2048,
|
|
35
|
+
) -> dict:
|
|
36
|
+
"""Derive a full QLoRA training config from a base-model choice.
|
|
37
|
+
|
|
38
|
+
Hyperparameters scale with model size to keep a single 24GB-class card viable
|
|
39
|
+
(effective batch ~16-32). ``seq_len`` is clamped to the trainer's range.
|
|
40
|
+
"""
|
|
41
|
+
params_b = float(params_b or 7)
|
|
42
|
+
|
|
43
|
+
if params_b <= 3:
|
|
44
|
+
lora_r, batch, accum, lr = 16, 4, 4, 2e-4
|
|
45
|
+
elif params_b <= 9:
|
|
46
|
+
lora_r, batch, accum, lr = 16, 2, 8, 2e-4
|
|
47
|
+
else: # 14B and up
|
|
48
|
+
lora_r, batch, accum, lr = 32, 1, 16, 1e-4
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"workflow": workflow,
|
|
52
|
+
"base_model_hf": base_model_hf,
|
|
53
|
+
"params_b": params_b,
|
|
54
|
+
"license": license or "",
|
|
55
|
+
"seq_len": clamp_seq_len(seq_len),
|
|
56
|
+
"lora_r": lora_r,
|
|
57
|
+
"lora_alpha": lora_r * 2,
|
|
58
|
+
"lora_dropout": 0.05,
|
|
59
|
+
"epochs": 3,
|
|
60
|
+
"lr": lr,
|
|
61
|
+
"per_device_batch_size": batch,
|
|
62
|
+
"grad_accum": accum,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _template(name: str) -> str:
|
|
67
|
+
return resources.files("blitz_cli.templates").joinpath(name).read_text(
|
|
68
|
+
encoding="utf-8"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _write(path: Path, content: str) -> None:
|
|
73
|
+
path.write_text(content, encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def render(out_dir: Path, project: str, workflow: str, endpoint: str) -> None:
|
|
77
|
+
"""Write the training project skeleton into out_dir.
|
|
78
|
+
|
|
79
|
+
No config.json is written: the container fetches the config from Blitz at run
|
|
80
|
+
start (see the templates). Only project/workflow/endpoint are substituted, all
|
|
81
|
+
into the Dockerfile/Makefile/README wiring.
|
|
82
|
+
"""
|
|
83
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
|
|
85
|
+
# Verbatim (read config.json at runtime) — keep as real source files.
|
|
86
|
+
for name in ("train.py", "requirements.txt", "dockerignore"):
|
|
87
|
+
dest = ".dockerignore" if name == "dockerignore" else name
|
|
88
|
+
_write(out_dir / dest, _template(name + ".tmpl"))
|
|
89
|
+
|
|
90
|
+
subs = {"project": project, "workflow": workflow, "endpoint": endpoint}
|
|
91
|
+
for name in ("Dockerfile", "Makefile", "README.md"):
|
|
92
|
+
_write(out_dir / name, Template(_template(name + ".tmpl")).safe_substitute(subs))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def gated_license_warning(license: str) -> str | None:
|
|
96
|
+
"""A warning string if a base model's license needs HF acceptance, else None."""
|
|
97
|
+
lic = (license or "").lower()
|
|
98
|
+
if lic.startswith("llama") or lic.startswith("gemma"):
|
|
99
|
+
return (
|
|
100
|
+
f"This base model is gated ({lic}). Accept its license on Hugging Face "
|
|
101
|
+
"and pass HF_TOKEN into the container (see README) before training."
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""`blitz skill install` — drop the Blitz Claude Code skills into a repo.
|
|
2
|
+
|
|
3
|
+
Each skill ships as package data under its own ``blitz_cli/skills/<name>`` dir
|
|
4
|
+
and is copied verbatim into a repo's ``.claude/skills`` so Claude Code can run
|
|
5
|
+
it in that repo.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib import resources
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SkillResult = tuple[str, Path, list[str], list[str]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_skill_names() -> list[str]:
|
|
17
|
+
"""Every skill packaged with this CLI, e.g. ["onboard-to-blitz", "configure-training"]."""
|
|
18
|
+
root = resources.files("blitz_cli.skills")
|
|
19
|
+
return sorted(
|
|
20
|
+
entry.name
|
|
21
|
+
for entry in root.iterdir()
|
|
22
|
+
if entry.is_dir() and entry.joinpath("SKILL.md").is_file()
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _iter_skill_files(name: str):
|
|
27
|
+
"""Yield (filename, text) for every file in the packaged skill ``name``."""
|
|
28
|
+
root = resources.files("blitz_cli.skills").joinpath(name)
|
|
29
|
+
for entry in root.iterdir():
|
|
30
|
+
if entry.is_file():
|
|
31
|
+
yield entry.name, entry.read_text(encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _install_one(dest_dir: Path, name: str, force: bool) -> SkillResult:
|
|
35
|
+
skill_dir = dest_dir / name
|
|
36
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
written: list[str] = []
|
|
38
|
+
skipped: list[str] = []
|
|
39
|
+
for filename, text in _iter_skill_files(name):
|
|
40
|
+
target = skill_dir / filename
|
|
41
|
+
if target.exists() and not force:
|
|
42
|
+
skipped.append(filename)
|
|
43
|
+
continue
|
|
44
|
+
target.write_text(text, encoding="utf-8")
|
|
45
|
+
written.append(filename)
|
|
46
|
+
return name, skill_dir, written, skipped
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def install(
|
|
50
|
+
dest_dir: Path, force: bool = False, names: list[str] | None = None
|
|
51
|
+
) -> list[SkillResult]:
|
|
52
|
+
"""Copy one or more packaged skills into ``dest_dir/<name>``.
|
|
53
|
+
|
|
54
|
+
Defaults to every packaged skill. Returns one ``(name, skill_dir, written,
|
|
55
|
+
skipped)`` tuple per skill — files newly written and files left untouched
|
|
56
|
+
because they already existed and ``force`` was False.
|
|
57
|
+
"""
|
|
58
|
+
targets = names if names is not None else list_skill_names()
|
|
59
|
+
return [_install_one(dest_dir, name, force) for name in targets]
|
|
@@ -30,50 +30,103 @@ def _cmd_scaffold(args: argparse.Namespace) -> int:
|
|
|
30
30
|
out = Path(args.out)
|
|
31
31
|
data_dir = out / "data"
|
|
32
32
|
|
|
33
|
-
try:
|
|
34
|
-
rec = client.recommended_base(args.workflow)
|
|
35
|
-
except BlitzAPIError as exc:
|
|
36
|
-
_eprint(f"error: {exc}")
|
|
37
|
-
return 1
|
|
38
|
-
|
|
39
|
-
model = rec["recommendation"]
|
|
40
|
-
print(f"Recommended base: {model['name']} ({model['hf']}) — {model.get('why', '')}")
|
|
41
|
-
warning = _scaffold.gated_license_warning(rec)
|
|
42
|
-
if warning:
|
|
43
|
-
_eprint(f"warning: {warning}")
|
|
44
|
-
|
|
45
33
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
46
34
|
try:
|
|
47
35
|
n_train = _stream_to_file(
|
|
48
36
|
client.download_dataset(args.workflow, args.include_synthetic),
|
|
49
37
|
data_dir / "dataset.jsonl",
|
|
50
38
|
)
|
|
51
|
-
n_eval = _stream_to_file(
|
|
52
|
-
client.download_eval_set(args.workflow), data_dir / "evalset.jsonl"
|
|
53
|
-
)
|
|
54
39
|
except BlitzAPIError as exc:
|
|
55
40
|
_eprint(f"error: {exc}")
|
|
56
41
|
return 1
|
|
57
42
|
|
|
58
43
|
print(f"Pulled {n_train} training examples → {data_dir / 'dataset.jsonl'}")
|
|
59
|
-
print(f"Pulled {n_eval} eval examples → {data_dir / 'evalset.jsonl'}")
|
|
60
44
|
if n_train == 0:
|
|
61
45
|
_eprint("error: no training examples for this workflow — nothing to train on.")
|
|
62
46
|
return 1
|
|
63
|
-
if n_eval == 0:
|
|
64
|
-
_eprint("warning: held-out eval set is empty — `make eval` will be skipped.")
|
|
65
47
|
|
|
66
|
-
|
|
48
|
+
_scaffold.render(out, args.project, args.workflow, args.endpoint)
|
|
67
49
|
print(f"Scaffolded training project → {out}/")
|
|
68
50
|
print(
|
|
69
51
|
"\nNext:\n"
|
|
52
|
+
f" blitz config set -p {args.project} -w {args.workflow} \\\n"
|
|
53
|
+
" --base <hf-model-id> --params-b <N> --seq-len <N> # push the training config\n"
|
|
70
54
|
f" cd {out}\n"
|
|
71
|
-
" export BLITZ_API_KEY=<your read key> #
|
|
55
|
+
" export BLITZ_API_KEY=<your read key> # container fetches config + posts results\n"
|
|
72
56
|
" make build\n"
|
|
73
57
|
" make train\n"
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
58
|
+
"\n(the container fetches config.json from Blitz at run start — set it first "
|
|
59
|
+
"with `blitz config set`)"
|
|
60
|
+
)
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cmd_config(args: argparse.Namespace) -> int:
|
|
65
|
+
api_key = args.api_key or os.environ.get("BLITZ_API_KEY")
|
|
66
|
+
if not api_key:
|
|
67
|
+
_eprint("error: no API key. Pass --api-key or set BLITZ_API_KEY (a read-scoped key).")
|
|
68
|
+
return 2
|
|
69
|
+
client = BlitzClient(endpoint=args.endpoint, api_key=api_key, project=args.project)
|
|
70
|
+
|
|
71
|
+
if args.config_cmd == "get":
|
|
72
|
+
try:
|
|
73
|
+
row = client.get_training_config(args.workflow)
|
|
74
|
+
except BlitzAPIError as exc:
|
|
75
|
+
_eprint(f"error: {exc}")
|
|
76
|
+
return 1
|
|
77
|
+
print(json.dumps(row["config"], indent=2))
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
# config_cmd == "set": build the config from --from, or derive from flags.
|
|
81
|
+
if args.from_file:
|
|
82
|
+
try:
|
|
83
|
+
config = json.loads(Path(args.from_file).read_text(encoding="utf-8"))
|
|
84
|
+
except (OSError, ValueError) as exc:
|
|
85
|
+
_eprint(f"error: could not read config from {args.from_file}: {exc}")
|
|
86
|
+
return 2
|
|
87
|
+
config.setdefault("workflow", args.workflow)
|
|
88
|
+
if config.get("workflow") != args.workflow:
|
|
89
|
+
_eprint(
|
|
90
|
+
f"error: --workflow {args.workflow!r} does not match config "
|
|
91
|
+
f"workflow {config.get('workflow')!r}."
|
|
92
|
+
)
|
|
93
|
+
return 2
|
|
94
|
+
if not config.get("base_model_hf"):
|
|
95
|
+
_eprint("error: config is missing base_model_hf.")
|
|
96
|
+
return 2
|
|
97
|
+
else:
|
|
98
|
+
if not args.base:
|
|
99
|
+
_eprint("error: pass --from <config.json>, or --base <hf-model-id> to derive a config.")
|
|
100
|
+
return 2
|
|
101
|
+
config = _scaffold.build_config(
|
|
102
|
+
workflow=args.workflow,
|
|
103
|
+
base_model_hf=args.base,
|
|
104
|
+
params_b=args.params_b if args.params_b is not None else 7.0,
|
|
105
|
+
license=args.license or "",
|
|
106
|
+
seq_len=args.seq_len if args.seq_len is not None else 2048,
|
|
107
|
+
)
|
|
108
|
+
# Optional per-flag hyperparameter overrides on top of the size-scaled defaults.
|
|
109
|
+
for key in ("epochs", "lr", "lora_r", "lora_alpha", "lora_dropout",
|
|
110
|
+
"per_device_batch_size", "grad_accum"):
|
|
111
|
+
val = getattr(args, key, None)
|
|
112
|
+
if val is not None:
|
|
113
|
+
config[key] = val
|
|
114
|
+
|
|
115
|
+
warning = _scaffold.gated_license_warning(config.get("license", ""))
|
|
116
|
+
if warning:
|
|
117
|
+
_eprint(f"warning: {warning}")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
row = client.set_training_config(config)
|
|
121
|
+
except BlitzAPIError as exc:
|
|
122
|
+
_eprint(f"error: {exc}")
|
|
123
|
+
return 1
|
|
124
|
+
stored = row["config"]
|
|
125
|
+
print(f"Pushed training config for {args.workflow} → project {args.project}")
|
|
126
|
+
print(
|
|
127
|
+
f"(base={stored['base_model_hf']} seq_len={stored['seq_len']} "
|
|
128
|
+
f"lora_r={stored['lora_r']} epochs={stored['epochs']} "
|
|
129
|
+
f"batch={stored['per_device_batch_size']} accum={stored['grad_accum']})"
|
|
77
130
|
)
|
|
78
131
|
return 0
|
|
79
132
|
|
|
@@ -230,17 +283,19 @@ def _cmd_skill(args: argparse.Namespace) -> int:
|
|
|
230
283
|
from blitz_cli import _skill
|
|
231
284
|
|
|
232
285
|
if getattr(args, "skill_cmd", None) != "install":
|
|
233
|
-
_eprint("usage: blitz skill install [--dir .claude/skills] [--force]")
|
|
286
|
+
_eprint("usage: blitz skill install [--dir .claude/skills] [--name NAME] [--force]")
|
|
234
287
|
return 2
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
288
|
+
names = [args.name] if getattr(args, "name", None) else None
|
|
289
|
+
results = _skill.install(Path(args.dir), force=args.force, names=names)
|
|
290
|
+
for name, skill_dir, written, skipped in results:
|
|
291
|
+
if written:
|
|
292
|
+
print(f"Installed {name} skill -> {skill_dir}/ ({', '.join(written)})")
|
|
293
|
+
else:
|
|
294
|
+
print(
|
|
295
|
+
f"Skill {name} already present at {skill_dir}/ "
|
|
296
|
+
f"({', '.join(skipped)}); use --force to overwrite."
|
|
297
|
+
)
|
|
298
|
+
print("Open Claude Code in this repo and run the installed skill(s).")
|
|
244
299
|
return 0
|
|
245
300
|
|
|
246
301
|
|
|
@@ -261,7 +316,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
261
316
|
|
|
262
317
|
sc = sub.add_parser(
|
|
263
318
|
"scaffold",
|
|
264
|
-
help="Pull a workflow's dataset +
|
|
319
|
+
help="Pull a workflow's dataset + emit a QLoRA training project skeleton.",
|
|
265
320
|
)
|
|
266
321
|
sc.add_argument("-p", "--project", required=True, help="Blitz project id")
|
|
267
322
|
sc.add_argument("-w", "--workflow", required=True, help="Workflow (root span) name")
|
|
@@ -283,6 +338,50 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
283
338
|
)
|
|
284
339
|
sc.set_defaults(func=_cmd_scaffold)
|
|
285
340
|
|
|
341
|
+
cfg = sub.add_parser(
|
|
342
|
+
"config",
|
|
343
|
+
help="Push or read a workflow's training config (the container fetches it at run).",
|
|
344
|
+
)
|
|
345
|
+
cfg.add_argument("-p", "--project", required=True, help="Blitz project id")
|
|
346
|
+
cfg.add_argument("-w", "--workflow", required=True, help="Workflow (root span) name")
|
|
347
|
+
cfg.add_argument(
|
|
348
|
+
"--endpoint",
|
|
349
|
+
default=os.environ.get("BLITZ_ENDPOINT", _DEFAULT_ENDPOINT),
|
|
350
|
+
help="Blitz API base URL (env BLITZ_ENDPOINT)",
|
|
351
|
+
)
|
|
352
|
+
cfg.add_argument(
|
|
353
|
+
"--api-key", default=None, help="Read-scoped API key (env BLITZ_API_KEY)"
|
|
354
|
+
)
|
|
355
|
+
cfg_sub = cfg.add_subparsers(dest="config_cmd", required=True)
|
|
356
|
+
|
|
357
|
+
cfg_get = cfg_sub.add_parser("get", help="Print the stored training config as JSON.")
|
|
358
|
+
cfg_get.set_defaults(func=_cmd_config)
|
|
359
|
+
|
|
360
|
+
cfg_set = cfg_sub.add_parser(
|
|
361
|
+
"set",
|
|
362
|
+
help="Push a training config: --from <file>, or --base/--params-b/--seq-len to derive.",
|
|
363
|
+
)
|
|
364
|
+
cfg_set.add_argument(
|
|
365
|
+
"--from", dest="from_file", default=None,
|
|
366
|
+
help="Path to a full config.json to push verbatim.",
|
|
367
|
+
)
|
|
368
|
+
cfg_set.add_argument("--base", default=None, help="Base model HF id (derive mode)")
|
|
369
|
+
cfg_set.add_argument("--params-b", dest="params_b", type=float, default=None,
|
|
370
|
+
help="Base model size in billions (drives hyperparameter defaults)")
|
|
371
|
+
cfg_set.add_argument("--license", default=None, help="Base model license (derive mode)")
|
|
372
|
+
cfg_set.add_argument("--seq-len", dest="seq_len", type=int, default=None,
|
|
373
|
+
help="Training sequence length (clamped 1024-8192)")
|
|
374
|
+
cfg_set.add_argument("--epochs", type=int, default=None, help="Override epochs")
|
|
375
|
+
cfg_set.add_argument("--lr", type=float, default=None, help="Override learning rate")
|
|
376
|
+
cfg_set.add_argument("--lora-r", dest="lora_r", type=int, default=None, help="Override LoRA rank")
|
|
377
|
+
cfg_set.add_argument("--lora-alpha", dest="lora_alpha", type=int, default=None, help="Override LoRA alpha")
|
|
378
|
+
cfg_set.add_argument("--lora-dropout", dest="lora_dropout", type=float, default=None, help="Override LoRA dropout")
|
|
379
|
+
cfg_set.add_argument("--batch-size", dest="per_device_batch_size", type=int, default=None,
|
|
380
|
+
help="Override per-device batch size")
|
|
381
|
+
cfg_set.add_argument("--grad-accum", dest="grad_accum", type=int, default=None,
|
|
382
|
+
help="Override gradient accumulation steps")
|
|
383
|
+
cfg_set.set_defaults(func=_cmd_config)
|
|
384
|
+
|
|
286
385
|
scn = sub.add_parser(
|
|
287
386
|
"scan",
|
|
288
387
|
help="Statically scan a codebase for AI/LLM calls and report Blitz "
|
|
@@ -353,17 +452,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
353
452
|
ver.set_defaults(func=_cmd_verify)
|
|
354
453
|
|
|
355
454
|
skl = sub.add_parser(
|
|
356
|
-
"skill", help="Install the Blitz Claude Code
|
|
455
|
+
"skill", help="Install the Blitz Claude Code skills into this repo."
|
|
357
456
|
)
|
|
358
457
|
skl_sub = skl.add_subparsers(dest="skill_cmd", required=True)
|
|
359
458
|
skl_install = skl_sub.add_parser(
|
|
360
|
-
"install", help="Copy the
|
|
459
|
+
"install", help="Copy the Blitz skills into .claude/skills/ (default: all of them)."
|
|
361
460
|
)
|
|
362
461
|
skl_install.add_argument(
|
|
363
462
|
"--dir",
|
|
364
463
|
default=".claude/skills",
|
|
365
464
|
help="Skills directory to install into (default .claude/skills)",
|
|
366
465
|
)
|
|
466
|
+
skl_install.add_argument(
|
|
467
|
+
"--name",
|
|
468
|
+
default=None,
|
|
469
|
+
help="Install only this skill (default: install every packaged skill)",
|
|
470
|
+
)
|
|
367
471
|
skl_install.add_argument(
|
|
368
472
|
"--force", action="store_true", help="Overwrite an existing skill copy"
|
|
369
473
|
)
|