From 032a83fcc3a400405fe9ad7610c84f5f77f02346 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:04:18 +0530 Subject: [PATCH 1/2] harness: default model for launches (wally default-models) + regroup --help by on-device vs cloud --- CMakeLists.txt | 2 + src/app.cpp | 42 ++++--- src/commands/cmd_default_models.cpp | 75 +++++++++++++ src/commands/cmd_editors.cpp | 5 +- src/commands/cmd_harness.cpp | 19 ++-- src/commands/commands.h | 7 ++ src/config/preferences.cpp | 163 ++++++++++++++++++++++++++++ src/config/preferences.h | 68 ++++++++++++ tests/test_wally_unit.cpp | 140 ++++++++++++++++++++++++ 9 files changed, 497 insertions(+), 24 deletions(-) create mode 100644 src/commands/cmd_default_models.cpp create mode 100644 src/config/preferences.cpp create mode 100644 src/config/preferences.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4028e34..4f6a8b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ set(WALLY_SOURCES src/commands/cmd_bench.cpp src/commands/cmd_editors.cpp src/commands/cmd_harness.cpp + src/commands/cmd_default_models.cpp src/commands/cmd_account.cpp src/commands/cmd_usage.cpp src/account/console.cpp @@ -81,6 +82,7 @@ set(WALLY_SOURCES src/commands/engine_options.cpp src/commands/model_setup.cpp src/config/cli_paths.cpp + src/config/preferences.cpp src/device_info.cpp src/net/control_plane.cpp src/net/loopback_auth.cpp diff --git a/src/app.cpp b/src/app.cpp index 7f60911..d92580d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -89,6 +89,7 @@ void configure_app(CLI::App& app, GlobalOptions& options) { commands::register_usage(app, options); commands::register_editors(app, options); commands::register_harness(app, options); + commands::register_default_models(app, options); commands::register_telemetry(app, options); // `--help` groups: CLI11 prints one heading per distinct group string, in @@ -96,22 +97,33 @@ void configure_app(CLI::App& app, GlobalOptions& options) { // this order is the print order. Centralized here rather than one // ->group() call per register_* file: 36 top-level commands with no // grouping at all used to land in a single default SUBCOMMANDS: bucket. + // Grouped so the split a reader cares about is visible at a glance: what + // runs on this machine, versus what talks to the hosted console. The + // coding agents sit between the two because they do both — a local model or + // a hosted one behind the same command — so they carry the "(local or + // hosted)" tag rather than landing in either camp. + constexpr const char* kGenerate = "Generate (on-device)"; + constexpr const char* kModels = "On-device models"; + constexpr const char* kAgents = "Coding agents (local or hosted)"; + constexpr const char* kCloud = "Cloud account"; + constexpr const char* kServe = "Serve & benchmark (on-device)"; + constexpr const char* kAbout = "About"; const std::vector> help_groups = { - {"llm", "Generate"}, {"vlm", "Generate"}, {"stt", "Generate"}, - {"tts", "Generate"}, {"vad", "Generate"}, {"embed", "Generate"}, - {"rerank", "Generate"}, {"image", "Generate"}, {"diarize", "Generate"}, - {"segment", "Generate"}, {"voice", "Generate"}, {"rag", "Generate"}, - {"run", "Shortcuts"}, {"chat", "Shortcuts"}, {"ls", "Shortcuts"}, - {"show", "Shortcuts"}, {"pull", "Shortcuts"}, {"rm", "Shortcuts"}, - {"models", "Models"}, {"lora", "Models"}, - {"serve", "Serve & measure"}, {"bench", "Serve & measure"}, - {"backends", "Serve & measure"}, {"info", "Serve & measure"}, - {"about", "Serve & measure"}, {"version", "Serve & measure"}, - {"auth", "Account"}, {"login", "Account"}, {"logout", "Account"}, - {"whoami", "Account"}, {"usage", "Account"}, - {"opencode", "Editors & agents"}, {"codex", "Editors & agents"}, - {"claude-code", "Editors & agents"}, {"claude-desktop", "Editors & agents"}, - {"clion", "Editors & agents"}, {"rustrover", "Editors & agents"}, + {"llm", kGenerate}, {"vlm", kGenerate}, {"stt", kGenerate}, + {"tts", kGenerate}, {"vad", kGenerate}, {"embed", kGenerate}, + {"rerank", kGenerate}, {"image", kGenerate}, {"diarize", kGenerate}, + {"segment", kGenerate}, {"voice", kGenerate}, {"rag", kGenerate}, + {"run", kModels}, {"chat", kModels}, {"ls", kModels}, + {"show", kModels}, {"pull", kModels}, {"rm", kModels}, + {"models", kModels}, {"lora", kModels}, + {"opencode", kAgents}, {"codex", kAgents}, + {"claude-code", kAgents}, {"claude-desktop", kAgents}, + {"clion", kAgents}, {"rustrover", kAgents}, + {"default-models", kAgents}, + {"auth", kCloud}, {"login", kCloud}, {"logout", kCloud}, + {"whoami", kCloud}, {"usage", kCloud}, + {"serve", kServe}, {"bench", kServe}, {"backends", kServe}, + {"info", kAbout}, {"about", kAbout}, {"version", kAbout}, }; // configure_app() runs ahead of run()'s own try/catch (and tests call it // directly with none at all), so a typo here must never propagate as an diff --git a/src/commands/cmd_default_models.cpp b/src/commands/cmd_default_models.cpp new file mode 100644 index 0000000..4efa540 --- /dev/null +++ b/src/commands/cmd_default_models.cpp @@ -0,0 +1,75 @@ +/** + * @file cmd_default_models.cpp + * @brief `wally default-models` — read, set, or clear the model a harness launch + * uses when the reader gives no `-m`. See `config/preferences.h` for the + * resolution rule. + */ + +#include "commands/commands.h" + +#include +#include + +#include "config/preferences.h" +#include "io/output.h" + +namespace wally::commands { + +std::string ResolveDefaultModel(const std::string& explicit_model) { + const std::string effective = prefs::ResolveModel(explicit_model); + // Announce only when a default filled in for an omitted -m, so the launch + // never silently picks a model the reader did not name. + if (explicit_model.empty() && !effective.empty()) { + out::status_line("using default model " + effective); + } + return effective; +} + +void register_default_models(CLI::App& app, GlobalOptions& options) { + static_cast(options); + auto model = std::make_shared(); + auto clear = std::make_shared(false); + + CLI::App* cmd = app.add_subcommand( + "default-models", "Set the model a harness uses when you pass no -m"); + cmd->add_option("model", *model, "a model id to make the default, e.g. glm-5.3-flash"); + cmd->add_flag("--clear", *clear, "remove the saved default"); + + cmd->callback([model, clear] { + if (*clear) { + std::string error; + if (!prefs::ClearDefaultModel(&error)) { + out::error_line(error); + throw CLI::RuntimeError(1); + } + out::status_line("default model cleared"); + return; + } + + if (!model->empty()) { + std::string error; + if (!prefs::SetDefaultModel(*model, &error)) { + out::error_line(error); + throw CLI::RuntimeError(1); + } + out::status_line("default model set to " + *model); + return; + } + + // No argument: report what is in effect and why. + const prefs::DefaultModel current = prefs::EffectiveDefaultModel(); + switch (current.source) { + case prefs::DefaultModelSource::Environment: + out::result_line(current.id + " (from WALLY_DEFAULT_MODEL)"); + break; + case prefs::DefaultModelSource::File: + out::result_line(current.id); + break; + case prefs::DefaultModelSource::None: + out::status_line("no default model set; pass one to save it"); + break; + } + }); +} + +} // namespace wally::commands diff --git a/src/commands/cmd_editors.cpp b/src/commands/cmd_editors.cpp index cdecb82..c40f44c 100644 --- a/src/commands/cmd_editors.cpp +++ b/src/commands/cmd_editors.cpp @@ -411,8 +411,9 @@ void register_editors(CLI::App& app, GlobalOptions& options) { fail(Restore(editor)); return; } - fail(*serve ? Serve(*model, options.verbose) - : Run(editor, *model, *rest, options.verbose)); + const std::string effective = ResolveDefaultModel(*model); + fail(*serve ? Serve(effective, options.verbose) + : Run(editor, effective, *rest, options.verbose)); }); } } diff --git a/src/commands/cmd_harness.cpp b/src/commands/cmd_harness.cpp index 51dab1b..87f007a 100644 --- a/src/commands/cmd_harness.cpp +++ b/src/commands/cmd_harness.cpp @@ -39,15 +39,18 @@ void register_harness(CLI::App& app, GlobalOptions& options) { opencode->add_option("args", *rest, "passed through to opencode")->allow_extra_args(); opencode->prefix_command(); opencode->callback([model, rest, cloud] { + const std::string effective = ResolveDefaultModel(*model); if (*cloud) { - if (model->empty()) { - out::error_line("--cloud requires --model "); + if (effective.empty()) { + out::error_line( + "--cloud requires --model , and no default is set " + "(wally default-models )"); fail(2); } - fail(harness::LaunchOpenCodeCloud(*model, *rest)); + fail(harness::LaunchOpenCodeCloud(effective, *rest)); return; } - fail(harness::Launch("opencode", *model, *rest)); + fail(harness::Launch("opencode", effective, *rest)); }); // Codex speaks the Responses API, which the hosted gateway serves and a @@ -61,11 +64,13 @@ void register_harness(CLI::App& app, GlobalOptions& options) { codex->add_option("args", *codex_rest, "passed through to codex")->allow_extra_args(); codex->prefix_command(); codex->callback([codex_model, codex_rest] { - if (codex_model->empty()) { - out::error_line("wally codex needs a hosted model: wally codex -m "); + const std::string effective = ResolveDefaultModel(*codex_model); + if (effective.empty()) { + out::error_line("wally codex needs a hosted model: wally codex -m " + "(or set one with wally default-models )"); fail(2); } - fail(harness::LaunchCodexCloud(*codex_model, *codex_rest)); + fail(harness::LaunchCodexCloud(effective, *codex_rest)); }); } diff --git a/src/commands/commands.h b/src/commands/commands.h index 7dfc77a..d2ec7ef 100644 --- a/src/commands/commands.h +++ b/src/commands/commands.h @@ -87,6 +87,13 @@ void register_account(CLI::App& app, GlobalOptions& options); void register_usage(CLI::App& app, GlobalOptions& options); void register_editors(CLI::App& app, GlobalOptions& options); void register_harness(CLI::App& app, GlobalOptions& options); +void register_default_models(CLI::App& app, GlobalOptions& options); + +/// The model a harness launch should use: `explicit_model` when the reader gave +/// one, otherwise the saved default (see `config/preferences.h`). Prints a +/// status line when a default fills in for an omitted `-m`, so no launch picks a +/// model silently. Shared by the harness and editor commands. +std::string ResolveDefaultModel(const std::string& explicit_model); void register_telemetry(CLI::App& app, GlobalOptions& options); /** diff --git a/src/config/preferences.cpp b/src/config/preferences.cpp new file mode 100644 index 0000000..33324bd --- /dev/null +++ b/src/config/preferences.cpp @@ -0,0 +1,163 @@ +#include "config/preferences.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "account/credentials.h" +#include "harness/harness.h" +#include "io/output.h" + +namespace wally::prefs { +namespace { + +namespace fs = std::filesystem; +using Json = nlohmann::json; + +constexpr const char* kFileName = "preferences.json"; +constexpr const char* kDefaultModelKey = "default_model"; + +std::string EnvValue(const char* name) { + const char* value = std::getenv(name); + return value == nullptr ? std::string() : std::string(value); +} + +/// Reads and parses the preferences file. Returns an empty object for a missing +/// file, and — on a parse error — an empty object plus a stderr warning, so a +/// corrupt file degrades to "no preferences" rather than breaking a launch. +Json ReadFile() { + const std::string path = PreferencesPath(); + if (path.empty()) { + return Json::object(); + } + std::ifstream file(path, std::ios::binary); + if (!file.good()) { + return Json::object(); + } + Json parsed = Json::parse(file, nullptr, /*allow_exceptions=*/false); + if (parsed.is_discarded() || !parsed.is_object()) { + out::status_line("ignoring unreadable preferences file at " + path); + return Json::object(); + } + return parsed; +} + +/// Writes `document` to the preferences file atomically: a sibling temp file +/// then a rename, so a reader never sees a half-written file. +bool WriteFile(const Json& document, std::string* error) { + const std::string path = PreferencesPath(); + if (path.empty()) { + if (error != nullptr) { + *error = "cannot resolve a home directory for preferences"; + } + return false; + } + + const fs::path target(path); + std::error_code ec; + fs::create_directories(target.parent_path(), ec); + if (ec) { + if (error != nullptr) { + *error = "cannot create " + target.parent_path().string() + ": " + ec.message(); + } + return false; + } + + const fs::path temp = target.parent_path() / (std::string(kFileName) + ".tmp"); + { + std::ofstream out(temp, std::ios::binary | std::ios::trunc); + if (!out.good()) { + if (error != nullptr) { + *error = "cannot write " + temp.string(); + } + return false; + } + out << document.dump(2) << '\n'; + out.flush(); + if (!out.good()) { + if (error != nullptr) { + *error = "failed writing " + temp.string(); + } + return false; + } + } + + fs::rename(temp, target, ec); + if (ec) { + fs::remove(temp, ec); + if (error != nullptr) { + *error = "cannot replace " + target.string() + ": " + ec.message(); + } + return false; + } + return true; +} + +} // namespace + +std::string PreferencesPath() { + const std::string directory = account::ProfileDirectory(); + if (directory.empty()) { + return {}; + } + return (fs::path(directory) / kFileName).string(); +} + +std::optional FileDefaultModel() { + const Json document = ReadFile(); + const auto it = document.find(kDefaultModelKey); + if (it == document.end() || !it->is_string()) { + return std::nullopt; + } + const auto value = it->get(); + if (value.empty()) { + return std::nullopt; + } + return value; +} + +DefaultModel EffectiveDefaultModel() { + const std::string env = EnvValue("WALLY_DEFAULT_MODEL"); + if (!env.empty()) { + return {env, DefaultModelSource::Environment}; + } + if (const std::optional file = FileDefaultModel()) { + return {*file, DefaultModelSource::File}; + } + return {}; +} + +std::string ResolveModel(const std::string& explicit_model) { + if (!explicit_model.empty()) { + return explicit_model; + } + return EffectiveDefaultModel().id; +} + +bool SetDefaultModel(const std::string& id, std::string* error) { + if (!harness::ModelIdIsSafe(id)) { + if (error != nullptr) { + *error = "not a usable model id: " + id; + } + return false; + } + Json document = ReadFile(); + document[kDefaultModelKey] = id; + return WriteFile(document, error); +} + +bool ClearDefaultModel(std::string* error) { + Json document = ReadFile(); + if (!document.contains(kDefaultModelKey)) { + return true; + } + document.erase(kDefaultModelKey); + return WriteFile(document, error); +} + +} // namespace wally::prefs diff --git a/src/config/preferences.h b/src/config/preferences.h new file mode 100644 index 0000000..41926bc --- /dev/null +++ b/src/config/preferences.h @@ -0,0 +1,68 @@ +/** + * @file preferences.h + * @brief Non-secret per-profile CLI preferences. + * + * Distinct from the credential store (`account::`), which is the secret file at + * mode 0600. Preferences are plain configuration: what model a harness launch + * should use when the reader gives no `-m`. They live in the same profile dir as + * credentials so they follow `WALLY_PROFILE_DIR`, but in their own file. + * + * The only key today is the default model. Resolution precedence, highest first: + * 1. an explicit model the reader passed + * 2. the `WALLY_DEFAULT_MODEL` environment variable (a one-off shell override) + * 3. `default_model` in the preferences file + * 4. nothing — the caller keeps its no-model behaviour + */ + +#ifndef WALLY_CONFIG_PREFERENCES_H +#define WALLY_CONFIG_PREFERENCES_H + +#include +#include +#include + +namespace wally::prefs { + +/// {ProfileDirectory}/preferences.json, or empty when $HOME is unresolvable. +std::string PreferencesPath(); + +/// Where the default model came from, so a command can tell the reader which +/// value is winning and why clearing the file might not change anything. +enum class DefaultModelSource : std::uint8_t { + None, ///< no default anywhere + Environment, ///< WALLY_DEFAULT_MODEL is set + File, ///< default_model in the preferences file +}; + +struct DefaultModel { + std::string id; + DefaultModelSource source = DefaultModelSource::None; + bool set() const { return source != DefaultModelSource::None; } +}; + +/// The default model in effect: the env override if present, else the file. +/// A malformed file is treated as no default (a warning goes to stderr); it +/// never throws, so it cannot break a launch. +DefaultModel EffectiveDefaultModel(); + +/// The default model recorded in the file, ignoring the environment override. +/// This is what a `--clear` acts on and what "set" reports back. +std::optional FileDefaultModel(); + +/// The model a launch should use given what the reader typed. Returns +/// `explicit_model` unchanged when it is non-empty, otherwise the effective +/// default, otherwise empty. +std::string ResolveModel(const std::string& explicit_model); + +/// Persist `id` as the default model. Rejects an id that fails +/// `harness::ModelIdIsSafe`. Does not check any catalog: a hosted id such as +/// `glm-5.3-flash` is not in the local table, so real validity is left to the +/// launch path. Returns false with `*error` set on a bad id or a write failure. +bool SetDefaultModel(const std::string& id, std::string* error); + +/// Remove the default model from the file. Succeeds when no default was set. +bool ClearDefaultModel(std::string* error); + +} // namespace wally::prefs + +#endif // WALLY_CONFIG_PREFERENCES_H diff --git a/tests/test_wally_unit.cpp b/tests/test_wally_unit.cpp index b6b9e8b..6451595 100644 --- a/tests/test_wally_unit.cpp +++ b/tests/test_wally_unit.cpp @@ -37,6 +37,7 @@ #include "commands/engine_options.h" #include "commands/model_labels.h" #include "config/cli_paths.h" +#include "config/preferences.h" #include "io/image_io.h" #include "io/output.h" #include "io/proto.h" @@ -2401,6 +2402,143 @@ TestResult test_model_labels_format() { return result; } +// A private WALLY_PROFILE_DIR so the preferences tests never read or write the +// real profile. Removed on scope exit. +class TempProfileDir { +public: + TempProfileDir() { + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + path_ = std::filesystem::temp_directory_path() / + ("wally-prefs-" + std::to_string(stamp)); + std::error_code ec; + std::filesystem::remove_all(path_, ec); + std::filesystem::create_directories(path_, ec); + dir_str_ = path_.string(); + } + ~TempProfileDir() { + std::error_code ec; + std::filesystem::remove_all(path_, ec); + } + const char *c_str() const { return dir_str_.c_str(); } + +private: + std::filesystem::path path_; + std::string dir_str_; +}; + +TestResult test_default_model_resolution() { + TestResult result; + result.test_name = "default_model_resolution"; + + TempProfileDir dir; + EnvVar profile("WALLY_PROFILE_DIR", dir.c_str()); + EnvVar legacy("RCLI_PROFILE_DIR", nullptr); + + { + EnvVar env_default("WALLY_DEFAULT_MODEL", nullptr); + // Fresh: no default anywhere. An explicit model passes through; an empty + // one stays empty (the caller keeps its no-model behaviour). + if (wally::prefs::ResolveModel("qwen3-0.6b") != "qwen3-0.6b") { + result.details = "explicit model should pass through unchanged"; + return result; + } + if (!wally::prefs::ResolveModel("").empty()) { + result.details = "empty input with no default should stay empty"; + return result; + } + + std::string error; + if (!wally::prefs::SetDefaultModel("glm-5.3-flash", &error)) { + result.details = "SetDefaultModel failed: " + error; + return result; + } + // File default fills in for an omitted model, but never overrides explicit. + if (wally::prefs::ResolveModel("") != "glm-5.3-flash") { + result.details = "file default should fill in for an empty model"; + return result; + } + if (wally::prefs::ResolveModel("qwen3-0.6b") != "qwen3-0.6b") { + result.details = "explicit model should beat the file default"; + return result; + } + } + { + // The environment override outranks the file. + EnvVar env_default("WALLY_DEFAULT_MODEL", "env-model"); + const wally::prefs::DefaultModel effective = wally::prefs::EffectiveDefaultModel(); + if (effective.id != "env-model" || + effective.source != wally::prefs::DefaultModelSource::Environment) { + result.details = "WALLY_DEFAULT_MODEL should outrank the file"; + return result; + } + if (wally::prefs::ResolveModel("") != "env-model") { + result.details = "env default should resolve for an empty model"; + return result; + } + } + { + // With the env override gone, the file default is back. + EnvVar env_default("WALLY_DEFAULT_MODEL", nullptr); + if (wally::prefs::EffectiveDefaultModel().source != + wally::prefs::DefaultModelSource::File) { + result.details = "file default should return once the env override is gone"; + return result; + } + } + + result.passed = true; + return result; +} + +TestResult test_default_model_store() { + TestResult result; + result.test_name = "default_model_store"; + + TempProfileDir dir; + EnvVar profile("WALLY_PROFILE_DIR", dir.c_str()); + EnvVar legacy("RCLI_PROFILE_DIR", nullptr); + EnvVar env_default("WALLY_DEFAULT_MODEL", nullptr); + + std::string error; + // Set then clear round-trips, and clearing an already-empty default is fine. + if (!wally::prefs::SetDefaultModel("glm-5.3-flash", &error) || + wally::prefs::FileDefaultModel().value_or("") != "glm-5.3-flash") { + result.details = "set did not round-trip: " + error; + return result; + } + if (!wally::prefs::ClearDefaultModel(&error) || + wally::prefs::FileDefaultModel().has_value()) { + result.details = "clear did not remove the default: " + error; + return result; + } + if (!wally::prefs::ClearDefaultModel(&error)) { + result.details = "clearing an already-empty default should succeed"; + return result; + } + + // An unsafe or empty id is rejected, and nothing is written. + if (wally::prefs::SetDefaultModel("bad", &error) || + wally::prefs::SetDefaultModel("", &error) || + wally::prefs::FileDefaultModel().has_value()) { + result.details = "an unsafe or empty id should be rejected"; + return result; + } + + // A corrupt preferences file degrades to "no default", never a throw. + { + std::ofstream corrupt(wally::prefs::PreferencesPath(), std::ios::binary | std::ios::trunc); + corrupt << "{ this is not json"; + } + if (wally::prefs::FileDefaultModel().has_value() || + wally::prefs::EffectiveDefaultModel().set()) { + result.details = "a corrupt file should resolve to no default"; + return result; + } + + result.passed = true; + return result; +} + } // namespace int main(int argc, char **argv) { @@ -2443,5 +2581,7 @@ int main(int argc, char **argv) { suite.add("models_ls_is_primary_name", test_models_ls_is_primary_name); suite.add("loopback_token", test_loopback_token); suite.add("model_labels_format", test_model_labels_format); + suite.add("default_model_resolution", test_default_model_resolution); + suite.add("default_model_store", test_default_model_store); return suite.run(argc, argv); } From b90d58510e73a815adc0aa457dd85707edf8b6f5 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:04:18 +0530 Subject: [PATCH 2/2] release: 0.5.4 --- Formula/wally.rb | 6 +++--- versions.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Formula/wally.rb b/Formula/wally.rb index 86e24b8..6fa73c6 100644 --- a/Formula/wally.rb +++ b/Formula/wally.rb @@ -3,15 +3,15 @@ class Wally < Formula desc "Run language, speech and image models on your own machine" homepage "https://github.com/RunanywhereAI/wally" - version "0.5.3" + version "0.5.4" license "MIT" # macOS arm64 ships the Swift MLX host # (llama.cpp + ONNX + Sherpa + MLX). Linux is not in this cut. on_macos do on_arm do - url "https://github.com/RunanywhereAI/wally/releases/download/v0.5.3/wally-0.5.3-macos-arm64.tar.gz" - # Placeholder -- no v0.5.3 release has published a wally-named asset + url "https://github.com/RunanywhereAI/wally/releases/download/v0.5.4/wally-0.5.4-macos-arm64.tar.gz" + # Placeholder -- the v0.5.4 release has not published a wally-named asset # yet. scripts/release/update-tap.sh re-stamps this from the real release # checksum; a stale value here fails brew install's own hash check # rather than installing something unverified. diff --git a/versions.toml b/versions.toml index 63de5df..383394a 100644 --- a/versions.toml +++ b/versions.toml @@ -7,7 +7,7 @@ [product] # wally's own release version; git tag is v (auto-tag.yml reads it here). -version = "0.5.3" +version = "0.5.4" [sdk] # The published C++ desktop kit this tree builds against; bump the SHAs with it.