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.
Files changed (37) hide show
  1. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/PKG-INFO +6 -11
  2. blitz_cli-0.4.0/README.md +22 -0
  3. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/__init__.py +7 -6
  4. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_client.py +16 -28
  5. blitz_cli-0.4.0/blitz_cli/_scaffold.py +103 -0
  6. blitz_cli-0.4.0/blitz_cli/_skill.py +59 -0
  7. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/cli.py +140 -36
  8. blitz_cli-0.4.0/blitz_cli/skills/configure-training/SKILL.md +166 -0
  9. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/skills/onboard-to-blitz/SKILL.md +8 -2
  10. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/Dockerfile.tmpl +1 -2
  11. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/Makefile.tmpl +5 -7
  12. blitz_cli-0.4.0/blitz_cli/templates/README.md.tmpl +38 -0
  13. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/dockerignore.tmpl +2 -0
  14. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/requirements.txt.tmpl +1 -1
  15. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/train.py.tmpl +46 -14
  16. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/pyproject.toml +1 -1
  17. blitz_cli-0.4.0/tests/test_scaffold.py +89 -0
  18. blitz_cli-0.3.0/README.md +0 -27
  19. blitz_cli-0.3.0/blitz_cli/_scaffold.py +0 -111
  20. blitz_cli-0.3.0/blitz_cli/_skill.py +0 -42
  21. blitz_cli-0.3.0/blitz_cli/templates/README.md.tmpl +0 -36
  22. blitz_cli-0.3.0/blitz_cli/templates/eval.py.tmpl +0 -105
  23. blitz_cli-0.3.0/tests/test_scaffold.py +0 -80
  24. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/.github/workflows/homebrew.yml +0 -0
  25. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  26. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/.gitignore +0 -0
  27. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/LICENSE +0 -0
  28. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/__init__.py +0 -0
  29. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_classify.py +0 -0
  30. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_js.py +0 -0
  31. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_models.py +0 -0
  32. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_python.py +0 -0
  33. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_report.py +0 -0
  34. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/_scan/_walk.py +0 -0
  35. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/skills/__init__.py +0 -0
  36. {blitz_cli-0.3.0 → blitz_cli-0.4.0}/blitz_cli/templates/__init__.py +0 -0
  37. {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.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 + the recommended open base
14
- model, and scaffold a self-contained, runnable QLoRA training container — then
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 && make eval
23
+ make build && make train
25
24
  ```
26
25
 
27
- `blitz scaffold` writes `./train` with `data/dataset.jsonl`, `data/evalset.jsonl`,
28
- `config.json` (derived from the base-model recommendation), and a `Dockerfile` +
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`` Claude Code skill into
16
- the repo's ``.claude/skills`` so Claude can run the onboarding playbook.
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, held-out eval set, and base-model recommendation, then
20
- emit a self-contained, runnable training project (Dockerfile + train.py +
21
- eval.py). Authenticates with a read-scoped Blitz API key (``BLITZ_API_KEY``).
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.3.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 _post_json(self, path: str, body: dict) -> dict:
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="POST",
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
- # -- public API (one method per endpoint the CLI / eval loop needs) -----
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 recommended_base(self, workflow: str) -> dict:
112
- return self._get_json(self._p("/recommended-base"), {"workflow": workflow})
117
+ def get_training_config(self, workflow: str) -> dict:
118
+ return self._get_json(self._p("/training-config"), {"workflow": workflow})
113
119
 
114
- def recommended_tools(self, workflow: str) -> dict:
115
- return self._get_json(self._p("/recommended-tools"), {"workflow": workflow})
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 download_eval_set(self, workflow: Optional[str] = None) -> Iterator[dict]:
129
- return self._get_ndjson(self._p("/eval-dataset"), {"workflow": workflow})
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
- config = _scaffold.render(out, rec, args.project, args.workflow, args.endpoint)
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> # used by `make eval` to post results\n"
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
- " make eval\n"
75
- f"\n(base={config['base_model_hf']} seq_len={config['seq_len']} "
76
- f"lora_r={config['lora_r']} epochs={config['epochs']})"
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
- skill_dir, written, skipped = _skill.install(Path(args.dir), force=args.force)
236
- if written:
237
- print(f"Installed onboard-to-blitz skill -> {skill_dir}/ ({', '.join(written)})")
238
- else:
239
- print(
240
- f"Skill already present at {skill_dir}/ "
241
- f"({', '.join(skipped)}); use --force to overwrite."
242
- )
243
- print("Open Claude Code in this repo and run the onboard-to-blitz skill.")
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 + recommendation and emit a QLoRA training project.",
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 onboarding skill into this repo."
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 onboard-to-blitz skill into .claude/skills/."
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
  )