onde-cli 0.3.1__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.
- onde_cli-0.4.0/.agents/AGENTS.md +69 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.agents/skills/distribution/SKILL.md +17 -9
- onde_cli-0.4.0/.agents/skills/git/SKILL.md +70 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-github.yml +15 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-pub.yml +0 -112
- onde_cli-0.4.0/AGENTS.md +69 -0
- onde_cli-0.4.0/CLAUDE.md +69 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/Cargo.lock +522 -537
- {onde_cli-0.3.1 → onde_cli-0.4.0}/Cargo.toml +9 -7
- {onde_cli-0.3.1 → onde_cli-0.4.0}/LICENSE +1 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/PKG-INFO +3 -3
- {onde_cli-0.3.1 → onde_cli-0.4.0}/README.md +17 -3
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/onde-cli/README.md +1 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/nuget/onde-cli/README.md +3 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/.gitignore +1 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/.pubignore +1 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/CHANGELOG.md +4 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/LICENSE +1 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/README.md +14 -4
- onde_cli-0.4.0/pub/onde_cli/lib/onde_cli.dart +216 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/pubspec.yaml +2 -3
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pypi/README.md +1 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/app.rs +384 -33
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/chat.rs +7 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/finetune.rs +617 -35
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/gguf.rs +493 -42
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/gresiq.rs +35 -1
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/hf_search.rs +3 -3
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/main.rs +8 -0
- onde_cli-0.4.0/src/mcp.rs +336 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/ui.rs +105 -21
- onde_cli-0.3.1/.agents/skills/git/SKILL.md +0 -43
- onde_cli-0.3.1/pub/onde_cli/lib/onde_cli.dart +0 -99
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-crates.yml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-homebrew.yml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-npm.yml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-nuget.yml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.github/workflows/release-pypi.yml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.gitignore +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.nuget/NuGet/NuGet.Config +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/.nvmrc +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/assets/ondeinference-apps.png +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/build.rs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/README.md.tmpl +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/onde-cli/.gitignore +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/onde-cli/package-lock.json +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/onde-cli/package.json +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/onde-cli/src/index.ts +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/onde-cli/tsconfig.json +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/package-main.json.tmpl +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/package.json.tmpl +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/scripts/render-main-package.cjs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/npm/scripts/render-platform-package.cjs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/nuget/.gitignore +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/nuget/onde-cli/Onde.Cli.csproj +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/nuget/onde-cli/Program.cs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pub/onde_cli/bin/onde.dart +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pypi/pyproject.toml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/pyproject.toml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/rust-toolchain.toml +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/hf.rs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/hf_clone.rs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/hf_upload.rs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/merge.rs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/project.rs +0 -0
- {onde_cli-0.3.1 → onde_cli-0.4.0}/src/token.rs +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
`onde-cli` is a Rust TUI (`onde` binary) for managing an Onde Inference account and running a local model pipeline: LoRA fine-tune → adapter merge → GGUF export → local chat test → Hugging Face upload → model assignment to an Onde app. npm, PyPI, NuGet, pub.dev, Homebrew, and crates.io are thin wrappers around this one binary; the Rust crate is the source of truth.
|
|
4
|
+
|
|
5
|
+
## Build, test, lint
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
cargo build # debug build
|
|
9
|
+
cargo run # launch the TUI
|
|
10
|
+
cargo run -- --mcp # run as an MCP server over stdio (no TUI)
|
|
11
|
+
cargo test --locked # unit tests (heavy ones are #[ignore])
|
|
12
|
+
cargo clippy --all-targets -- -D warnings
|
|
13
|
+
cargo fmt -- --check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The build **requires** these env vars (read from `.env` via `build.rs`): `ONDE_APP_ID`, `ONDE_APP_SECRET`, `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`. `HF_TOKEN` is optional. They are baked in at compile time via `env!(...)` in `src/app.rs`. Changing `.env` triggers a full rebuild.
|
|
17
|
+
|
|
18
|
+
On macOS, candle's `metal` + `accelerate` features are added automatically. **Fine-tuning always runs on CPU** — Metal's backward pass in candle 0.10.2 produces corrupt gradients (1e6–1e29 in magnitude, where CPU gives ~40–65 for identical losses). Use `ONDE_FINETUNE_METAL=1` only to test newer candle versions, and verify with `ONDE_FINETUNE_GRAD_DEBUG=1` (per-step loss/max_abs/norm).
|
|
19
|
+
|
|
20
|
+
Linux CI requires `libclang-dev` (`sudo apt-get install -y libclang-dev`).
|
|
21
|
+
|
|
22
|
+
### Tests that load real models
|
|
23
|
+
|
|
24
|
+
`src/gguf.rs` has two `#[ignore]` integration tests that export a ~1GB GGUF and run inference through `onde::mistralrs`. They need a Qwen3-0.6B model cached in the Onde App Group container and skip gracefully when it is absent.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
cargo test gguf::tests::exported_qwen3_gguf_is_runnable -- --ignored --nocapture
|
|
28
|
+
cargo test gguf::tests::finetune_merge_export_run -- --ignored --nocapture
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Env knobs for these: `ONDE_TEST_MODEL_DIR`, `ONDE_TEST_DTYPE` (`f16`/`q8_0`), `ONDE_TEST_LR`, `ONDE_TEST_DATA_PATH`, `ONDE_TEST_EPOCHS`, `ONDE_TEST_MAX_SEQ_LEN`, `ONDE_TEST_EVAL_PROMPT` (`||`-separated prompts), `ONDE_TEST_SYSTEM_PROMPT`. This makes the full-pipeline test double as a headless fine-tune runner for a custom dataset.
|
|
32
|
+
|
|
33
|
+
### Debugging the TUI
|
|
34
|
+
|
|
35
|
+
`main.rs` redirects both stdout and stderr to `~/.cache/onde/debug.log` before ratatui takes the alternate screen, because `mistral.rs` writes to both fds and would otherwise tear up the display. To see what the app or the inference engine is doing, tail that log; `println!`/`eprintln!`/`log::*` all land there, not on screen.
|
|
36
|
+
|
|
37
|
+
## Architecture — `src/`
|
|
38
|
+
|
|
39
|
+
| File(s) | Responsibility |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `main.rs` | Entry point; redirects stdout/stderr to `~/.cache/onde/debug.log` before ratatui takes over |
|
|
42
|
+
| `app.rs` | `App` struct + `Screen` state machine + `AuthEvent` channel (`tokio::select!` over an `mpsc` channel and a `crossterm` `EventStream`); `App::apply` folds background progress into state |
|
|
43
|
+
| `ui.rs` | Pure ratatui render of `App`; no SDK imports (re-exports types through `app.rs`) |
|
|
44
|
+
| `finetune.rs` | Hand-written LoRA trainer (candle, CPU): Qwen forward pass (RMSNorm, RoPE, GQA, optional Qwen3 QK-norm), LoRA A/B trained on q/v projections in F32, writes `lora_adapter.safetensors`. Gradients are sanitized (non-finite elements zeroed) and globally norm-clipped in a scaled space before each AdamW step; a step is skipped (loudly) only when the norm is zero or non-finite. **Skipping this guard corrupts every weight after the first bad step.** |
|
|
45
|
+
| `merge.rs` | Folds LoRA adapter into base weights (`W + scale·(B@A)`); writes `model.safetensors` plus copied config/tokenizer |
|
|
46
|
+
| `gguf.rs` | Hand-rolled GGUF writer (no llama.cpp). Tensor dims are written innermost-first — reverse of safetensors shape, because candle reverses on read; `head_dim` comes from config and is emitted as `attention.key_length`/`value_length` (Qwen3 decouples it from `hidden_size/num_heads`); `token_type` is an INT32 array; `general.architecture` is chosen from `model_type` so Qwen3 routes to candle's `quantized_qwen3` loader |
|
|
47
|
+
| `chat.rs` | Local GGUF chat via `onde::mistralrs::GgufModelBuilder`, the same engine the Onde SDK uses, so a model can be tested before publishing |
|
|
48
|
+
| `hf_upload.rs` / `hf_clone.rs` / `hf_search.rs` / `hf.rs` | Hugging Face Hub: upload the GGUF, check/create a repo, search models, and resolve/merge the local HF cache (incl. the macOS Onde App Group container) |
|
|
49
|
+
| `gresiq.rs` | `smbcloud-gresiq-sdk` wrapper for apps and the model catalog. "Deploying" a model means `assign_model(app_id, model_id)` against a catalog entry; the end app then fetches that assignment through the Onde SDK's `load_assigned_model` and downloads the GGUF. The CLI can only assign models that already exist in the GresIQ catalog |
|
|
50
|
+
| `token.rs` | Auth token persistence |
|
|
51
|
+
| `project.rs` | Per-project fine-tune workspaces under `~/.onde` |
|
|
52
|
+
|
|
53
|
+
**Background task rule:** network/IO → `tokio::spawn`; CPU-heavy tensor work (`finetune`, `merge`, `gguf`) → `std::thread::spawn` to avoid starving the async runtime.
|
|
54
|
+
|
|
55
|
+
**Adding a feature pattern:** new `Screen` variant → key handler → `AuthEvent` variant → background task that streams progress events.
|
|
56
|
+
|
|
57
|
+
### The `onde` dependency
|
|
58
|
+
|
|
59
|
+
The `onde` crate provides the inference engine (`onde::mistralrs`, a vendored mistral.rs) and `onde::inference::models::SUPPORTED_MODEL_INFO` (the supported-model catalog the inference picker mirrors). It is normally the published crate; `Cargo.toml` has commented `path`/`[patch.crates-io]` blocks for developing against local checkouts of `onde`, the `smbcloud-*` crates, and `candle` — never commit with those uncommented.
|
|
60
|
+
|
|
61
|
+
## Conventions
|
|
62
|
+
|
|
63
|
+
- **Git:** merge feature/release/hotfix branches with `--no-ff`. Tag the merge commit on `main`. Never tag a branch tip that hasn't been merged. See `.agents/skills/git/SKILL.md`.
|
|
64
|
+
- **Commits:** include `Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com>` trailer when committing with the agent.
|
|
65
|
+
- **`[patch.crates-io]` and `path =` deps:** commented out in `Cargo.toml` for local dev against `onde`/`smbcloud-*`/`candle`. Never commit with these uncommented.
|
|
66
|
+
- **Distribution:** before touching any wrapper package (npm, PyPI, NuGet, pub.dev, Homebrew), read `.agents/skills/distribution/SKILL.md`. Keep all channel versions aligned with the Rust crate version in `Cargo.toml`.
|
|
67
|
+
- **Wrapper packages** are in `npm/`, `pypi/`, `nuget/`, `pub/`. The npm base manifest is rewritten at release time by `npm/scripts/render-main-package.cjs`; do not hand-edit committed package.json versions as the source of truth for releases.
|
|
68
|
+
- **Clippy** runs with `-D warnings` in CI; all lints must pass.
|
|
69
|
+
- **Rust edition:** 2024; toolchain: stable.
|
|
@@ -82,14 +82,16 @@ Good fit for .NET developers and CI environments that already use `dotnet tool i
|
|
|
82
82
|
|
|
83
83
|
### pub.dev
|
|
84
84
|
|
|
85
|
-
Use a Dart package with a command entrypoint and a library that launches the
|
|
85
|
+
Use a Dart package with a command entrypoint and a library that launches the native binary.
|
|
86
86
|
|
|
87
87
|
Important details:
|
|
88
88
|
|
|
89
89
|
- executable command should stay `onde`
|
|
90
|
-
- use `Isolate.resolvePackageUri` to find the installed package path
|
|
91
90
|
- keep the runtime-id mapping explicit
|
|
92
|
-
-
|
|
91
|
+
- download the correct binary from GitHub Releases on first run
|
|
92
|
+
- cache downloads under `~/.onde/cli`
|
|
93
|
+
- keep the wrapper thin, the Rust binary is still the product
|
|
94
|
+
- publish the pub.dev package only after the matching GitHub Release exists
|
|
93
95
|
|
|
94
96
|
Good fit for Dart and Flutter developers who want the same CLI through familiar tooling.
|
|
95
97
|
|
|
@@ -141,14 +143,18 @@ Safer checks:
|
|
|
141
143
|
|
|
142
144
|
pub.dev behaves differently from npm and NuGet in one important way:
|
|
143
145
|
|
|
144
|
-
-
|
|
146
|
+
- large bundled binaries quickly hit package size limits
|
|
145
147
|
|
|
146
|
-
That
|
|
148
|
+
That makes the safest design a thin Dart launcher that downloads the right native binary from GitHub Releases at runtime instead of shipping every platform binary inside the package.
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
Practical consequences:
|
|
149
151
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
+
- `pub/onde_cli/native/` should stay out of the published package
|
|
153
|
+
- do not set `publish_to: https://pub.dev` in `pubspec.yaml`; omit `publish_to` for normal pub.dev publishing
|
|
154
|
+
- use package-level ignore rules so local native staging does not leak into publish input
|
|
155
|
+
- keep release asset names aligned with the GitHub Release workflow, because the launcher depends on them
|
|
156
|
+
- treat `~/.onde/cli` as the local cache root for downloaded binaries
|
|
157
|
+
- do not let the pub.dev workflow race ahead of GitHub Releases; dispatch it after `release-github.yml` has created the assets
|
|
152
158
|
|
|
153
159
|
Run these during verification:
|
|
154
160
|
|
|
@@ -211,7 +217,9 @@ At minimum, check:
|
|
|
211
217
|
- manually bumping the committed npm manifest and assuming that is what npm publishes
|
|
212
218
|
- staged native binaries land in the wrong directory before packaging
|
|
213
219
|
- package builds successfully but installs without the native binary
|
|
214
|
-
- pub.dev
|
|
220
|
+
- pub.dev launcher points at the wrong GitHub Release asset name or runtime id
|
|
221
|
+
- pub.dev workflow publishes before the matching GitHub Release assets exist
|
|
222
|
+
- pub.dev package still includes bundled binaries and blows the upload size limit
|
|
215
223
|
- smoke test invokes a TUI-first binary in a way that does not make sense for CI
|
|
216
224
|
- README badges and install commands drift out of sync with real package names
|
|
217
225
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git
|
|
3
|
+
description: Git workflow rules for this repository.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep, AskUserQuestion
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Git skill
|
|
9
|
+
|
|
10
|
+
## Core rule
|
|
11
|
+
|
|
12
|
+
- Use `--no-ff`. Important.
|
|
13
|
+
|
|
14
|
+
## What this means in practice
|
|
15
|
+
|
|
16
|
+
- Do not fast-forward merge feature branches, release branches, or hotfix branches.
|
|
17
|
+
- Prefer explicit merge commits so the branch history stays visible.
|
|
18
|
+
- Release prep goes through `development` on its way to `main`, with `--no-ff` at every hop.
|
|
19
|
+
- Tag the merge commit that contains the final release state.
|
|
20
|
+
|
|
21
|
+
## Branch flow
|
|
22
|
+
|
|
23
|
+
Feature branches merge into `development`. Releases go:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
release/vX.Y.Z -> development -> main
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Never merge a release branch straight into `main` — `development` has to carry the
|
|
30
|
+
release state too, or the next feature branch starts from a base that is missing it.
|
|
31
|
+
|
|
32
|
+
## Release guidance
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 1. cut the release branch from development, commit the version bumps
|
|
36
|
+
git checkout development
|
|
37
|
+
git checkout -b release/vX.Y.Z
|
|
38
|
+
git commit -am "Prepare vX.Y.Z"
|
|
39
|
+
|
|
40
|
+
# 2. release branch -> development
|
|
41
|
+
git checkout development
|
|
42
|
+
git merge --no-ff release/vX.Y.Z -m "Merge release prep for vX.Y.Z"
|
|
43
|
+
|
|
44
|
+
# 3. development -> main
|
|
45
|
+
git checkout main
|
|
46
|
+
git merge --no-ff development -m "Merge development for vX.Y.Z"
|
|
47
|
+
|
|
48
|
+
# 4. tag the main merge commit, then push
|
|
49
|
+
git tag vX.Y.Z
|
|
50
|
+
git push origin development
|
|
51
|
+
git push origin main
|
|
52
|
+
git push origin vX.Y.Z
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The tag belongs on the step-3 merge commit on `main`, not on the release branch and not
|
|
56
|
+
on `development`. Pushing the tag is what triggers the wrapper publish workflows, which
|
|
57
|
+
read the version from the tag.
|
|
58
|
+
|
|
59
|
+
If the work is already on `main`, make a normal commit and tag that commit. Do not rewrite history just to avoid a merge commit.
|
|
60
|
+
|
|
61
|
+
## Avoid
|
|
62
|
+
|
|
63
|
+
- `git merge --ff`
|
|
64
|
+
- `git pull --ff-only` as a default recommendation for this repo's merge policy
|
|
65
|
+
- Tagging a branch tip that has not been merged into `main` when the intended release source of truth is `main`
|
|
66
|
+
- Merging a release branch directly into `main`, skipping `development`
|
|
67
|
+
|
|
68
|
+
## Why
|
|
69
|
+
|
|
70
|
+
The repo wants explicit history. Release commits should be easy to find, easy to audit, and clearly connected to the branch that introduced them.
|
|
@@ -190,3 +190,18 @@ jobs:
|
|
|
190
190
|
}
|
|
191
191
|
})
|
|
192
192
|
console.log('Dispatched release-homebrew.yml for tag ${{ steps.tag.outputs.tag }}')
|
|
193
|
+
|
|
194
|
+
- name: Trigger pub.dev release
|
|
195
|
+
uses: actions/github-script@v7
|
|
196
|
+
with:
|
|
197
|
+
script: |
|
|
198
|
+
await github.rest.actions.createWorkflowDispatch({
|
|
199
|
+
owner: context.repo.owner,
|
|
200
|
+
repo: context.repo.repo,
|
|
201
|
+
workflow_id: 'release-pub.yml',
|
|
202
|
+
ref: 'main',
|
|
203
|
+
inputs: {
|
|
204
|
+
tag: '${{ steps.tag.outputs.tag }}'
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
console.log('Dispatched release-pub.yml for tag ${{ steps.tag.outputs.tag }}')
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
name: pub.dev Release
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*.*.*"
|
|
7
4
|
workflow_dispatch:
|
|
8
5
|
inputs:
|
|
9
6
|
tag:
|
|
@@ -16,98 +13,11 @@ permissions:
|
|
|
16
13
|
env:
|
|
17
14
|
CARGO_TERM_COLOR: always
|
|
18
15
|
PUB_PACKAGE_NAME: onde_cli
|
|
19
|
-
ONDE_APP_ID: ${{ secrets.ONDE_APP_ID }}
|
|
20
|
-
ONDE_APP_SECRET: ${{ secrets.ONDE_APP_SECRET }}
|
|
21
|
-
GRESIQ_API_KEY: ${{ secrets.GRESIQ_API_KEY }}
|
|
22
|
-
GRESIQ_API_SECRET: ${{ secrets.GRESIQ_API_SECRET }}
|
|
23
|
-
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
|
24
16
|
|
|
25
17
|
jobs:
|
|
26
|
-
build-native-binaries:
|
|
27
|
-
name: Build native binary (${{ matrix.build.NAME }})
|
|
28
|
-
runs-on: ${{ matrix.build.OS }}
|
|
29
|
-
strategy:
|
|
30
|
-
fail-fast: false
|
|
31
|
-
matrix:
|
|
32
|
-
build:
|
|
33
|
-
- {
|
|
34
|
-
NAME: linux-x64,
|
|
35
|
-
OS: ubuntu-latest,
|
|
36
|
-
TARGET: x86_64-unknown-linux-gnu,
|
|
37
|
-
}
|
|
38
|
-
- {
|
|
39
|
-
NAME: linux-arm64,
|
|
40
|
-
OS: ubuntu-24.04-arm,
|
|
41
|
-
TARGET: aarch64-unknown-linux-gnu,
|
|
42
|
-
}
|
|
43
|
-
- {
|
|
44
|
-
NAME: windows-x64,
|
|
45
|
-
OS: windows-2022,
|
|
46
|
-
TARGET: x86_64-pc-windows-msvc,
|
|
47
|
-
}
|
|
48
|
-
- {
|
|
49
|
-
NAME: windows-arm64,
|
|
50
|
-
OS: windows-2022,
|
|
51
|
-
TARGET: aarch64-pc-windows-msvc,
|
|
52
|
-
}
|
|
53
|
-
- {
|
|
54
|
-
NAME: darwin-x64,
|
|
55
|
-
OS: macos-26-intel,
|
|
56
|
-
TARGET: x86_64-apple-darwin,
|
|
57
|
-
}
|
|
58
|
-
- { NAME: darwin-arm64, OS: macos-26, TARGET: aarch64-apple-darwin }
|
|
59
|
-
steps:
|
|
60
|
-
- name: Checkout
|
|
61
|
-
uses: actions/checkout@v6
|
|
62
|
-
with:
|
|
63
|
-
ref: ${{ github.event.inputs.tag || github.ref }}
|
|
64
|
-
|
|
65
|
-
- name: Read Rust toolchain
|
|
66
|
-
shell: bash
|
|
67
|
-
run: |
|
|
68
|
-
rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
|
|
69
|
-
if [ -z "$rust_toolchain" ]; then
|
|
70
|
-
echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
|
|
71
|
-
exit 1
|
|
72
|
-
fi
|
|
73
|
-
echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
|
|
74
|
-
|
|
75
|
-
- name: Install Rust toolchain
|
|
76
|
-
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
|
|
77
|
-
with:
|
|
78
|
-
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
|
79
|
-
targets: ${{ matrix.build.TARGET }}
|
|
80
|
-
|
|
81
|
-
- name: Setup Rust cache
|
|
82
|
-
uses: Swatinem/rust-cache@v2
|
|
83
|
-
with:
|
|
84
|
-
key: pub-${{ matrix.build.TARGET }}
|
|
85
|
-
|
|
86
|
-
- name: Build binary
|
|
87
|
-
shell: bash
|
|
88
|
-
run: cargo build --locked --release --target ${{ matrix.build.TARGET }}
|
|
89
|
-
|
|
90
|
-
- name: Prepare artifact
|
|
91
|
-
shell: bash
|
|
92
|
-
run: |
|
|
93
|
-
executable_name="onde"
|
|
94
|
-
if [[ "${{ matrix.build.OS }}" == "windows-2022" ]]; then
|
|
95
|
-
executable_name="onde.exe"
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
mkdir -p "pub-artifacts/native/${{ matrix.build.NAME }}"
|
|
99
|
-
cp "target/${{ matrix.build.TARGET }}/release/${executable_name}" "pub-artifacts/native/${{ matrix.build.NAME }}/${executable_name}"
|
|
100
|
-
|
|
101
|
-
- name: Upload native artifact
|
|
102
|
-
uses: actions/upload-artifact@v4
|
|
103
|
-
with:
|
|
104
|
-
name: pub-native-${{ matrix.build.NAME }}
|
|
105
|
-
path: pub-artifacts/native
|
|
106
|
-
|
|
107
18
|
verify-package:
|
|
108
19
|
name: Verify Dart package
|
|
109
20
|
runs-on: ubuntu-latest
|
|
110
|
-
needs: build-native-binaries
|
|
111
21
|
steps:
|
|
112
22
|
- name: Checkout
|
|
113
23
|
uses: actions/checkout@v6
|
|
@@ -125,17 +35,6 @@ jobs:
|
|
|
125
35
|
fi
|
|
126
36
|
echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
|
|
127
37
|
|
|
128
|
-
- name: Download native artifacts
|
|
129
|
-
uses: actions/download-artifact@v4
|
|
130
|
-
with:
|
|
131
|
-
pattern: pub-native-*
|
|
132
|
-
path: pub/onde_cli/native
|
|
133
|
-
merge-multiple: true
|
|
134
|
-
|
|
135
|
-
- name: Stage generated native binaries
|
|
136
|
-
shell: bash
|
|
137
|
-
run: git add -f pub/onde_cli/native
|
|
138
|
-
|
|
139
38
|
- name: Setup Dart SDK
|
|
140
39
|
uses: dart-lang/setup-dart@v1
|
|
141
40
|
|
|
@@ -187,17 +86,6 @@ jobs:
|
|
|
187
86
|
fi
|
|
188
87
|
echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
|
|
189
88
|
|
|
190
|
-
- name: Download native artifacts
|
|
191
|
-
uses: actions/download-artifact@v4
|
|
192
|
-
with:
|
|
193
|
-
pattern: pub-native-*
|
|
194
|
-
path: pub/onde_cli/native
|
|
195
|
-
merge-multiple: true
|
|
196
|
-
|
|
197
|
-
- name: Stage generated native binaries
|
|
198
|
-
shell: bash
|
|
199
|
-
run: git add -f pub/onde_cli/native
|
|
200
|
-
|
|
201
89
|
- name: Setup Dart SDK
|
|
202
90
|
uses: dart-lang/setup-dart@v1
|
|
203
91
|
|
onde_cli-0.4.0/AGENTS.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
`onde-cli` is a Rust TUI (`onde` binary) for managing an Onde Inference account and running a local model pipeline: LoRA fine-tune → adapter merge → GGUF export → local chat test → Hugging Face upload → model assignment to an Onde app. npm, PyPI, NuGet, pub.dev, Homebrew, and crates.io are thin wrappers around this one binary; the Rust crate is the source of truth.
|
|
4
|
+
|
|
5
|
+
## Build, test, lint
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
cargo build # debug build
|
|
9
|
+
cargo run # launch the TUI
|
|
10
|
+
cargo run -- --mcp # run as an MCP server over stdio (no TUI)
|
|
11
|
+
cargo test --locked # unit tests (heavy ones are #[ignore])
|
|
12
|
+
cargo clippy --all-targets -- -D warnings
|
|
13
|
+
cargo fmt -- --check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The build **requires** these env vars (read from `.env` via `build.rs`): `ONDE_APP_ID`, `ONDE_APP_SECRET`, `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`. `HF_TOKEN` is optional. They are baked in at compile time via `env!(...)` in `src/app.rs`. Changing `.env` triggers a full rebuild.
|
|
17
|
+
|
|
18
|
+
On macOS, candle's `metal` + `accelerate` features are added automatically. **Fine-tuning always runs on CPU** — Metal's backward pass in candle 0.10.2 produces corrupt gradients (1e6–1e29 in magnitude, where CPU gives ~40–65 for identical losses). Use `ONDE_FINETUNE_METAL=1` only to test newer candle versions, and verify with `ONDE_FINETUNE_GRAD_DEBUG=1` (per-step loss/max_abs/norm).
|
|
19
|
+
|
|
20
|
+
Linux CI requires `libclang-dev` (`sudo apt-get install -y libclang-dev`).
|
|
21
|
+
|
|
22
|
+
### Tests that load real models
|
|
23
|
+
|
|
24
|
+
`src/gguf.rs` has two `#[ignore]` integration tests that export a ~1GB GGUF and run inference through `onde::mistralrs`. They need a Qwen3-0.6B model cached in the Onde App Group container and skip gracefully when it is absent.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
cargo test gguf::tests::exported_qwen3_gguf_is_runnable -- --ignored --nocapture
|
|
28
|
+
cargo test gguf::tests::finetune_merge_export_run -- --ignored --nocapture
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Env knobs for these: `ONDE_TEST_MODEL_DIR`, `ONDE_TEST_DTYPE` (`f16`/`q8_0`), `ONDE_TEST_LR`, `ONDE_TEST_DATA_PATH`, `ONDE_TEST_EPOCHS`, `ONDE_TEST_MAX_SEQ_LEN`, `ONDE_TEST_EVAL_PROMPT` (`||`-separated prompts), `ONDE_TEST_SYSTEM_PROMPT`. This makes the full-pipeline test double as a headless fine-tune runner for a custom dataset.
|
|
32
|
+
|
|
33
|
+
### Debugging the TUI
|
|
34
|
+
|
|
35
|
+
`main.rs` redirects both stdout and stderr to `~/.cache/onde/debug.log` before ratatui takes the alternate screen, because `mistral.rs` writes to both fds and would otherwise tear up the display. To see what the app or the inference engine is doing, tail that log; `println!`/`eprintln!`/`log::*` all land there, not on screen.
|
|
36
|
+
|
|
37
|
+
## Architecture — `src/`
|
|
38
|
+
|
|
39
|
+
| File(s) | Responsibility |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `main.rs` | Entry point; redirects stdout/stderr to `~/.cache/onde/debug.log` before ratatui takes over |
|
|
42
|
+
| `app.rs` | `App` struct + `Screen` state machine + `AuthEvent` channel (`tokio::select!` over an `mpsc` channel and a `crossterm` `EventStream`); `App::apply` folds background progress into state |
|
|
43
|
+
| `ui.rs` | Pure ratatui render of `App`; no SDK imports (re-exports types through `app.rs`) |
|
|
44
|
+
| `finetune.rs` | Hand-written LoRA trainer (candle, CPU): Qwen forward pass (RMSNorm, RoPE, GQA, optional Qwen3 QK-norm), LoRA A/B trained on q/v projections in F32, writes `lora_adapter.safetensors`. Gradients are sanitized (non-finite elements zeroed) and globally norm-clipped in a scaled space before each AdamW step; a step is skipped (loudly) only when the norm is zero or non-finite. **Skipping this guard corrupts every weight after the first bad step.** |
|
|
45
|
+
| `merge.rs` | Folds LoRA adapter into base weights (`W + scale·(B@A)`); writes `model.safetensors` plus copied config/tokenizer |
|
|
46
|
+
| `gguf.rs` | Hand-rolled GGUF writer (no llama.cpp). Tensor dims are written innermost-first — reverse of safetensors shape, because candle reverses on read; `head_dim` comes from config and is emitted as `attention.key_length`/`value_length` (Qwen3 decouples it from `hidden_size/num_heads`); `token_type` is an INT32 array; `general.architecture` is chosen from `model_type` so Qwen3 routes to candle's `quantized_qwen3` loader |
|
|
47
|
+
| `chat.rs` | Local GGUF chat via `onde::mistralrs::GgufModelBuilder`, the same engine the Onde SDK uses, so a model can be tested before publishing |
|
|
48
|
+
| `hf_upload.rs` / `hf_clone.rs` / `hf_search.rs` / `hf.rs` | Hugging Face Hub: upload the GGUF, check/create a repo, search models, and resolve/merge the local HF cache (incl. the macOS Onde App Group container) |
|
|
49
|
+
| `gresiq.rs` | `smbcloud-gresiq-sdk` wrapper for apps and the model catalog. "Deploying" a model means `assign_model(app_id, model_id)` against a catalog entry; the end app then fetches that assignment through the Onde SDK's `load_assigned_model` and downloads the GGUF. The CLI can only assign models that already exist in the GresIQ catalog |
|
|
50
|
+
| `token.rs` | Auth token persistence |
|
|
51
|
+
| `project.rs` | Per-project fine-tune workspaces under `~/.onde` |
|
|
52
|
+
|
|
53
|
+
**Background task rule:** network/IO → `tokio::spawn`; CPU-heavy tensor work (`finetune`, `merge`, `gguf`) → `std::thread::spawn` to avoid starving the async runtime.
|
|
54
|
+
|
|
55
|
+
**Adding a feature pattern:** new `Screen` variant → key handler → `AuthEvent` variant → background task that streams progress events.
|
|
56
|
+
|
|
57
|
+
### The `onde` dependency
|
|
58
|
+
|
|
59
|
+
The `onde` crate provides the inference engine (`onde::mistralrs`, a vendored mistral.rs) and `onde::inference::models::SUPPORTED_MODEL_INFO` (the supported-model catalog the inference picker mirrors). It is normally the published crate; `Cargo.toml` has commented `path`/`[patch.crates-io]` blocks for developing against local checkouts of `onde`, the `smbcloud-*` crates, and `candle` — never commit with those uncommented.
|
|
60
|
+
|
|
61
|
+
## Conventions
|
|
62
|
+
|
|
63
|
+
- **Git:** merge feature/release/hotfix branches with `--no-ff`. Tag the merge commit on `main`. Never tag a branch tip that hasn't been merged. See `.agents/skills/git/SKILL.md`.
|
|
64
|
+
- **Commits:** include `Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com>` trailer when committing with the agent.
|
|
65
|
+
- **`[patch.crates-io]` and `path =` deps:** commented out in `Cargo.toml` for local dev against `onde`/`smbcloud-*`/`candle`. Never commit with these uncommented.
|
|
66
|
+
- **Distribution:** before touching any wrapper package (npm, PyPI, NuGet, pub.dev, Homebrew), read `.agents/skills/distribution/SKILL.md`. Keep all channel versions aligned with the Rust crate version in `Cargo.toml`.
|
|
67
|
+
- **Wrapper packages** are in `npm/`, `pypi/`, `nuget/`, `pub/`. The npm base manifest is rewritten at release time by `npm/scripts/render-main-package.cjs`; do not hand-edit committed package.json versions as the source of truth for releases.
|
|
68
|
+
- **Clippy** runs with `-D warnings` in CI; all lints must pass.
|
|
69
|
+
- **Rust edition:** 2024; toolchain: stable.
|
onde_cli-0.4.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
`onde-cli` is a Rust TUI (`onde` binary) for managing an Onde Inference account and running a local model pipeline: LoRA fine-tune → adapter merge → GGUF export → local chat test → Hugging Face upload → model assignment to an Onde app. npm, PyPI, NuGet, pub.dev, Homebrew, and crates.io are thin wrappers around this one binary; the Rust crate is the source of truth.
|
|
4
|
+
|
|
5
|
+
## Build, test, lint
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
cargo build # debug build
|
|
9
|
+
cargo run # launch the TUI
|
|
10
|
+
cargo run -- --mcp # run as an MCP server over stdio (no TUI)
|
|
11
|
+
cargo test --locked # unit tests (heavy ones are #[ignore])
|
|
12
|
+
cargo clippy --all-targets -- -D warnings
|
|
13
|
+
cargo fmt -- --check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The build **requires** these env vars (read from `.env` via `build.rs`): `ONDE_APP_ID`, `ONDE_APP_SECRET`, `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`. `HF_TOKEN` is optional. They are baked in at compile time via `env!(...)` in `src/app.rs`. Changing `.env` triggers a full rebuild.
|
|
17
|
+
|
|
18
|
+
On macOS, candle's `metal` + `accelerate` features are added automatically. **Fine-tuning always runs on CPU** — Metal's backward pass in candle 0.10.2 produces corrupt gradients (1e6–1e29 in magnitude, where CPU gives ~40–65 for identical losses). Use `ONDE_FINETUNE_METAL=1` only to test newer candle versions, and verify with `ONDE_FINETUNE_GRAD_DEBUG=1` (per-step loss/max_abs/norm).
|
|
19
|
+
|
|
20
|
+
Linux CI requires `libclang-dev` (`sudo apt-get install -y libclang-dev`).
|
|
21
|
+
|
|
22
|
+
### Tests that load real models
|
|
23
|
+
|
|
24
|
+
`src/gguf.rs` has two `#[ignore]` integration tests that export a ~1GB GGUF and run inference through `onde::mistralrs`. They need a Qwen3-0.6B model cached in the Onde App Group container and skip gracefully when it is absent.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
cargo test gguf::tests::exported_qwen3_gguf_is_runnable -- --ignored --nocapture
|
|
28
|
+
cargo test gguf::tests::finetune_merge_export_run -- --ignored --nocapture
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Env knobs for these: `ONDE_TEST_MODEL_DIR`, `ONDE_TEST_DTYPE` (`f16`/`q8_0`), `ONDE_TEST_LR`, `ONDE_TEST_DATA_PATH`, `ONDE_TEST_EPOCHS`, `ONDE_TEST_MAX_SEQ_LEN`, `ONDE_TEST_EVAL_PROMPT` (`||`-separated prompts), `ONDE_TEST_SYSTEM_PROMPT`. This makes the full-pipeline test double as a headless fine-tune runner for a custom dataset.
|
|
32
|
+
|
|
33
|
+
### Debugging the TUI
|
|
34
|
+
|
|
35
|
+
`main.rs` redirects both stdout and stderr to `~/.cache/onde/debug.log` before ratatui takes the alternate screen, because `mistral.rs` writes to both fds and would otherwise tear up the display. To see what the app or the inference engine is doing, tail that log; `println!`/`eprintln!`/`log::*` all land there, not on screen.
|
|
36
|
+
|
|
37
|
+
## Architecture — `src/`
|
|
38
|
+
|
|
39
|
+
| File(s) | Responsibility |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `main.rs` | Entry point; redirects stdout/stderr to `~/.cache/onde/debug.log` before ratatui takes over |
|
|
42
|
+
| `app.rs` | `App` struct + `Screen` state machine + `AuthEvent` channel (`tokio::select!` over an `mpsc` channel and a `crossterm` `EventStream`); `App::apply` folds background progress into state |
|
|
43
|
+
| `ui.rs` | Pure ratatui render of `App`; no SDK imports (re-exports types through `app.rs`) |
|
|
44
|
+
| `finetune.rs` | Hand-written LoRA trainer (candle, CPU): Qwen forward pass (RMSNorm, RoPE, GQA, optional Qwen3 QK-norm), LoRA A/B trained on q/v projections in F32, writes `lora_adapter.safetensors`. Gradients are sanitized (non-finite elements zeroed) and globally norm-clipped in a scaled space before each AdamW step; a step is skipped (loudly) only when the norm is zero or non-finite. **Skipping this guard corrupts every weight after the first bad step.** |
|
|
45
|
+
| `merge.rs` | Folds LoRA adapter into base weights (`W + scale·(B@A)`); writes `model.safetensors` plus copied config/tokenizer |
|
|
46
|
+
| `gguf.rs` | Hand-rolled GGUF writer (no llama.cpp). Tensor dims are written innermost-first — reverse of safetensors shape, because candle reverses on read; `head_dim` comes from config and is emitted as `attention.key_length`/`value_length` (Qwen3 decouples it from `hidden_size/num_heads`); `token_type` is an INT32 array; `general.architecture` is chosen from `model_type` so Qwen3 routes to candle's `quantized_qwen3` loader |
|
|
47
|
+
| `chat.rs` | Local GGUF chat via `onde::mistralrs::GgufModelBuilder`, the same engine the Onde SDK uses, so a model can be tested before publishing |
|
|
48
|
+
| `hf_upload.rs` / `hf_clone.rs` / `hf_search.rs` / `hf.rs` | Hugging Face Hub: upload the GGUF, check/create a repo, search models, and resolve/merge the local HF cache (incl. the macOS Onde App Group container) |
|
|
49
|
+
| `gresiq.rs` | `smbcloud-gresiq-sdk` wrapper for apps and the model catalog. "Deploying" a model means `assign_model(app_id, model_id)` against a catalog entry; the end app then fetches that assignment through the Onde SDK's `load_assigned_model` and downloads the GGUF. The CLI can only assign models that already exist in the GresIQ catalog |
|
|
50
|
+
| `token.rs` | Auth token persistence |
|
|
51
|
+
| `project.rs` | Per-project fine-tune workspaces under `~/.onde` |
|
|
52
|
+
|
|
53
|
+
**Background task rule:** network/IO → `tokio::spawn`; CPU-heavy tensor work (`finetune`, `merge`, `gguf`) → `std::thread::spawn` to avoid starving the async runtime.
|
|
54
|
+
|
|
55
|
+
**Adding a feature pattern:** new `Screen` variant → key handler → `AuthEvent` variant → background task that streams progress events.
|
|
56
|
+
|
|
57
|
+
### The `onde` dependency
|
|
58
|
+
|
|
59
|
+
The `onde` crate provides the inference engine (`onde::mistralrs`, a vendored mistral.rs) and `onde::inference::models::SUPPORTED_MODEL_INFO` (the supported-model catalog the inference picker mirrors). It is normally the published crate; `Cargo.toml` has commented `path`/`[patch.crates-io]` blocks for developing against local checkouts of `onde`, the `smbcloud-*` crates, and `candle` — never commit with those uncommented.
|
|
60
|
+
|
|
61
|
+
## Conventions
|
|
62
|
+
|
|
63
|
+
- **Git:** merge feature/release/hotfix branches with `--no-ff`. Tag the merge commit on `main`. Never tag a branch tip that hasn't been merged. See `.agents/skills/git/SKILL.md`.
|
|
64
|
+
- **Commits:** include `Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com>` trailer when committing with the agent.
|
|
65
|
+
- **`[patch.crates-io]` and `path =` deps:** commented out in `Cargo.toml` for local dev against `onde`/`smbcloud-*`/`candle`. Never commit with these uncommented.
|
|
66
|
+
- **Distribution:** before touching any wrapper package (npm, PyPI, NuGet, pub.dev, Homebrew), read `.agents/skills/distribution/SKILL.md`. Keep all channel versions aligned with the Rust crate version in `Cargo.toml`.
|
|
67
|
+
- **Wrapper packages** are in `npm/`, `pypi/`, `nuget/`, `pub/`. The npm base manifest is rewritten at release time by `npm/scripts/render-main-package.cjs`; do not hand-edit committed package.json versions as the source of truth for releases.
|
|
68
|
+
- **Clippy** runs with `-D warnings` in CI; all lints must pass.
|
|
69
|
+
- **Rust edition:** 2024; toolchain: stable.
|