Skip to content

Releases: bitphill/loopsmith

loopsmith v0.3.1

Choose a tag to compare

@github-actions github-actions released this 10 Sep 14:36

Added

  • The guided walk-through now runs in the browser too. loopsmith --web
    opens by asking which kind of smith you are and remembers the answer: an
    experienced one goes straight to the six-step editor, a new one gets the
    explanation, a list of working examples to start from, and then the same
    one-question-at-a-time flow --guided runs in a terminal, drawn as cards.

    • Sections are asked in guided/mod.rs::stages() order — identity, providers,
      goals, validations, stop gates, then every advanced section (A–J) behind its
      own opt-in card, exactly as the terminal gates them.
    • One field per card, carrying that field's own explanation. A repeating
      section collects entries in a single card behind a + and stays there until
      "This part is done", which is the browser's shape of the terminal's
      Add / Edit / Remove / Done menu.
    • A choice is a row of options rather than a numbered list: circles for one
      answer, squares for several, a dropdown once a list is too long to scan.
    • Providers are the machine scan the browser already had — the CLIs actually
      on PATH, pre-filled, with the Test button still there.
    • Loading an example fills the answers in without skipping the questions.
    • The wizard and the six-step editor are two views of one draft: Expert
      editor
      hands the half-filled config to the form, ⌘K switches back, and
      the review rail watches either way. Create stays gated on the real
      validator reporting no errors.

    This is a frontend change. The web backend already exposed everything it
    needs, and the field list is a declarative mirror of guided/sections.rs
    rather than a second implementation of it.

Fixed

  • The step panel never unmounted the step you had just left. MorphPanel
    drove its transition with AnimatePresence, whose exit phase never completes
    under React 19's StrictMode with the pinned motion release. Under
    mode="popLayout" that left every step ever visited mounted on top of each
    other — duplicate form controls, duplicate element ids, and stale cards still
    able to catch a click — and the failure was invisible because the newest step
    was drawn last. It now keys the view and lets React do the swap, so exactly
    one step is mounted at a time. This affected the existing six-step editor as
    well as the new wizard, and is asserted in the end-to-end suite.

Changed

  • One card treatment across the whole UI. Cards are a hairline ring and a
    soft drop rather than a hard border, on the 10px corner rather than the 14px
    one, with --card-spacing owning the inset of a card's header, content and
    footer in one place. Because the ring is a shadow it costs no layout, so a
    card nested against another edge shows one continuous line instead of two
    abutting borders.

  • CI rebuilds the web UI and fails if the committed src/web/dist is not what
    the sources produce. That directory is compiled into the binary with
    include_str!, so before this a frontend change that was not rebuilt shipped
    the previous UI with nothing anywhere failing.

  • README-DETAIL.md said 240 tests in its badge and 148 in its body; both now
    say 415, which is what the suite actually runs.

loopsmith v0.3.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 13:08

Added

  • loopsmith --guided (also loopsmith guided) — a terminal wizard that
    builds a loop by asking one question at a time.
    A third front end onto the
    same A–J config that new hands you as a file and --web paints in a
    browser, for a machine with no browser and a person who would rather not open
    an editor. It walks every section with the field's own explanation in place,
    shows each default in [brackets], and writes nothing until the finished
    config passes the same loopsmith validate a hand-written file would.

    • Agent CLIs already on PATH are offered as a numbered menu, pre-filled with
      a working argv, the right model list, and the environment they need — so a
      provider is usually one keystroke, not a remembered command line.
    • :back, :next, :help, and :quit work at every prompt; :quit offers
      to save a resumable draft. Choices are numbered and accept the number or the
      option's name.
    • Lists — goals, validations, providers — carry an Add / Edit / Remove / Done
      menu, so nothing has to be right the first time.
    • loopsmith guided --edit <file> loads an existing loop, shows every current
      value as the default, and writes the result back.
    • Works in a --no-default-features build: the provider catalog moved to
      src/catalog.rs (plain data, no async) so the wizard needs nothing from the
      web feature, and detection is a synchronous PATH scan.

    New dependency ctrlc handles Ctrl-C identically across macOS, Linux, and
    Windows.

loopsmith v0.2.2

Choose a tag to compare

@github-actions github-actions released this 27 Aug 03:15

Release plumbing. No change to the binary's behaviour.

Changed

  • Publishing moved off a laptop and into the release workflow, with no stored
    tokens anywhere.
    All three registries now authenticate by identity rather
    than by a secret: GitHub mints a short-lived OIDC token for the workflow run,
    the registry verifies it came from this repository and this workflow, and
    issues a credential that expires in minutes.

    That retires CARGO_REGISTRY_TOKEN, NPM_TOKEN and PYPI_TOKEN — three
    long-lived credentials that sat in plaintext in a shell profile readable by
    every process running as that user, never expired, and would have let anyone
    holding the string publish as the maintainer indefinitely.

  • npm packages are published with provenance. A Sigstore attestation binds
    the tarball to the exact commit and workflow run that produced it, which npm
    shows as a verified build badge. It is the answer to the question supply-chain
    scanners are actually asking when they fall back to counting downloads: does
    the code on the registry match the code in the repository?

  • The crates.io publish retries on index propagation. Publishing 0.2.1 by
    hand, loopsmith-provider failed because loopsmith-util had been accepted
    but was not yet visible to its verification build. It succeeded unchanged on a
    second attempt. Already-published is now treated as success too, so re-running
    a partially-completed release does not fall over on the crates that got
    through.

  • author is set on all three packages — missing entirely from npm, and a
    bare handle on crates.io.

loopsmith v0.2.1

Choose a tag to compare

@github-actions github-actions released this 26 Aug 20:38

Patch. One user-visible bug in loopsmith web, no behaviour change anywhere else.

Fixed

  • --port no longer claims a free port was busy. Starting the web UI with
    an explicit port printed (port 3210 was busy) even when nothing was using
    3210 and the server had bound it on the first attempt. The condition fired
    whenever the bound port differed from the 3000 default, which is true of every
    custom port by definition.

    That is worse than printing nothing, because it sends someone looking for a
    port conflict that does not exist. The decision is now a named function with
    all four cases pinned by a test: the default taken as asked, a custom port
    that was free, and each of the two ways of genuinely stepping past a busy one.

    Found by running the published 0.2.0 binary rather than a local build, which
    is the only reason it was caught at all.

loopsmith v0.2.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 20:09
871917f

Security

  • Closed a DNS-rebinding hole in loopsmith web. Binding loopback stops the
    network reaching the server; it does not stop a browser reaching it. A page
    on any domain whose DNS re-resolves to 127.0.0.1 is same-origin to the
    browser, so no CORS check ever runs and the request arrives looking ordinary.

    Verified exploitable before the fix: Host: evil.com returned 200 on every
    endpoint, and a cross-origin POST /api/jobs executed. That reachability
    meant POST /api/secrets/reveal could read back every API key in the shell
    profile, and POST /api/secrets could write a variable into it — which is
    code execution at the next login.

    The fix is the standard one: an attacker controls the DNS name but cannot
    change the Host header, because the browser sets it from the URL. Requests
    not addressed to a loopback name are refused, as are state-changing requests
    carrying a foreign Origin. The guard wraps the assets too, so a rebound page
    cannot even read the bundle.

  • Refused environment variables that are not credentials. The secrets panel
    accepted any all-caps name, including PATH, LD_PRELOAD,
    DYLD_INSERT_LIBRARIES, NODE_OPTIONS and GIT_SSH_COMMAND. Writing one of
    those into a shell profile arranges for code to run at the next login rather
    than storing a key. None of them is a credential, so refusing them costs a
    legitimate user nothing.

Fixed

  • A run no longer appears to stop when the tab closes. It never did stop —
    a job is a subprocess of the server, not of the page — but the page forgot
    which job it had been watching, which looked identical. It now reattaches to
    anything still running on load, and the socket's replay means the log arrives
    whole rather than from the moment of rejoining.
  • Mutex poisoning could permanently disable the job panel. Twelve
    lock().unwrap() call sites meant one panic inside any critical section
    poisoned the registry for the rest of the session. They now recover.
  • Unbounded process spawning. POST /api/jobs had no ceiling; a stuck
    button or a reloading page could spawn subprocesses until the machine gave up.
    Capped at eight concurrent.
  • Blocking work on the async workers. Handlers touching the filesystem, the
    keychain, or a subprocess ran on tokio worker threads. list_secrets was the
    worst: one security spawn per key, so a dozen sequential subprocesses on
    every page load, with the Keychain free to stop and prompt. Moved to
    spawn_blocking.

Fixed

  • Opening the memory store now waits for a lock that is being released.
    Sled holds a file lock and releases it as part of cleanup, which neither
    drop nor process exit makes instantaneous — so opening a store microseconds
    after the previous holder let go returned WouldBlock and failed outright.

    The CLI barely exposed this: each command opens once and exits, and watch
    opens once and reuses it. The web UI is what made it reachable, because
    pressing Run and then Status spawns two processes back to back and the first
    is often still exiting when the second opens. Caught by a macOS CI runner
    under load, on a test that had passed thirty consecutive times locally.

    open now retries for up to two seconds, backing off, and only for lock
    contention — a corrupt database or a missing directory still fails at once. A
    lock nobody releases reports which process is probably holding it rather than
    hanging.

Fixed (provider catalog)

  • A chosen model was silently discarded. The Claude and Grok entries listed
    models, the UI offered them, the user picked one — and the argv never
    mentioned {model}, so the CLI used its own default. Both now pass it.

  • Hermes was invoked with a flag it does not have. -p is not a Hermes
    option; -z / --oneshot is, and its own description is exactly what a loop
    wants: one prompt, only the final response on stdout, no banner or spinner.

  • llm would have been handed an empty model. It listed no models but
    substituted {model}, and an unset model renders as an empty string, so the
    CLI would receive a bare -m "". It now reads the prompt from stdin and uses
    its own configured default.

  • Three entries claimed to be verified without having been. gemini,
    codex and llm are not installed on the machine this table was checked
    against, so their argv was written from memory rather than from evidence.
    They are marked as templates, which is what that grade is for.

    Four invariants now hold the table honest: an entry may not substitute
    {model} unless it has one to offer, an entry that offers models must
    actually pass one, the prompt must be delivered exactly once by exactly one
    route, and a template must say so in its own note. Three of those four failed
    when first written, which is how the bugs above were found. Ollama is the one
    entry whose models are discovered at run time rather than listed, and that is
    now a field rather than an absence.

Changed

  • A new loop gets its own directory inside the folder you choose, named
    after the loop, rather than being scaffolded into that folder directly. The
    obvious thing to pick is a container like ~/loops, and scaffolding straight
    into it turned the container into the loop — after which every later loop
    either refused as non-empty or was forced on top of the first one's ledger.

  • loopsmith new --git initialises a repository, with one commit, in the
    new directory. This is what makes isolated: true isolate: a worktree is a
    second checkout, so with no repository every node shares one directory —
    fine for a single builder, destructive for two at once. The initial commit is
    not optional, because git worktree add resolves a start point and a
    repository with no HEAD has none. The web UI turns it on by default, which
    removes the "not inside a git repository" warning entirely. The scaffold has
    always written a .gitignore; nothing ever created the repository it implied.

  • Split release and dist build profiles. cargo install and Homebrew
    build on the user's machine while they wait, so release stays at thin LTO.
    The published npm and PyPI binaries are built once in CI, where nobody is
    waiting, so the release workflow now uses dist — fat LTO and one codegen
    unit. Measured on this workspace: 8.21 MB in 3m50s against 6.88 MB in 9m29s,
    16% off the download for build time that costs nothing.

    panic stays at unwind in both. The usual argument for abort is size,
    but loopsmith is a long-running server as well as a CLI, and tokio catches a
    panicking request handler to keep the rest alive; under abort one bad
    request would take down a server the user has open.

Added

Adds a browser UI. No change to the config model, the gate, or any existing
command's behaviour.

Added

  • loopsmith --web / loopsmith web — a local browser UI for building,
    checking, creating, and running loops. Both spellings resolve to one command;
    --web combined with a subcommand is refused rather than silently resolved.
    Serves 127.0.0.1:3000, steps up a port if that one is busy, and opens a tab.

    It exists for the reader README-FOR-DUMMIES.md was written for — the one who
    bounced off a schema reference. Every field carries a permanent one-line hint
    and an info control explaining why the field exists and what goes wrong
    without it
    , and a dismissible five-panel tour explains the one idea the rest
    depends on: that a model never certifies its own completion.

    Three properties hold the design up. It binds loopback only, because it spawns
    commands as this user. Every action spawns current_exe() rather than calling
    the crates, so the browser cannot drift from the CLI and cannot do anything
    loopsmith --help does not list — the browser names a verb from a closed list
    and never names a program. And the frontend is compiled into the binary, so an
    install from any registry has a working UI with nothing else to fetch.

  • Machine detection. Agent CLIs on PATH with their versions, Ollama models
    via ollama list, MCP servers read from ~/.claude.json,
    ~/.claude/settings.json, Claude Desktop, ~/.cursor/mcp.json, VS Code and
    ./.mcp.json, which API keys are present (presence only — values are never
    read), installed sub-agents, git, and the platform facts doctor reports.
    Found CLIs become one-click provider cards prefilling a known-good argv;
    entries whose argv is a starting point rather than verified say so on the card
    instead of failing later as a spawn error.

    A per-provider Test button performs a real handshake. It is a button and
    not part of detection because a page load is not consent to spend money.

  • Live review, recomputed in-process on every edit: loopsmith_core::validate
    issues with clickable field paths, the wave schedule and Amdahl ceiling from
    loopsmith_graph::plan, parallel builders that would overwrite each other for
    want of a worktree, the derived permission grant, and an upper-bound cost — or
    the word unbounded where no ceiling is set.

  • Secrets panel. Writes to the shell profile (a real environment variable,
    0600, inside a fenced block rewritten in place) or to the OS secret store —
    Keychain, Credential Manager, libsecret. The profile file is chosen from
    $SHELL, so a zsh login gets .zshrc rather than the .profile zsh never
    reads. Only the key name ever reaches a config, via requires_env.

  • The thirteen examples ship inside the binary and load in one click.
    tools/sync-examples.sh copies config/examples/*.yaml into the crate, since
    include_str! cannot reach above the package root and config/ is excluded
    from the published tarball. A test fails when the copies have drifted, so a
    stale example is caught by cargo test rather than by a user.

  • The logo in the UI — header, firs...

Read more

loopsmith v0.1.4

Choose a tag to compare

@github-actions github-actions released this 17 Aug 23:02

Documentation only. No behaviour, no API, no config-model change.

Added

  • README-FOR-DUMMIES.md — a start-to-finish guide for the people loopsmith
    is actually most useful to and who bounced off every existing page: marketers,
    salespeople, researchers, analysts. It assumes no shell fluency and no
    programming, and it stops at the point where a loop is running on a schedule.
    Everything about how the tool works internally is deliberately absent.

    It routes those readers down the Markdown path rather than the YAML one,
    and by seeding from a shipped example rather than from the blank starter —
    loopsmith new --config-file <example>.md writes loop.md and points
    run.sh at it, so the reader never has a second config file to be confused by.
    Scheduling is taught as interval seconds; cron is never mentioned, because a
    five-field expression is a wall for this reader and the trigger that avoids it
    already exists.

    It names six sections — B, C, D, F, G, H — as the whole
    editable surface, and reduces detectors to the two that need no programmer:
    file_exists and judge. It also tells the reader to delete every
    type: script check
    , which is the one edit standing between a copied example
    and a config that validates: the examples reference detector scripts the
    repository deliberately does not ship, and a non-programmer has no way to
    discover that from the error.

  • A TL;DR section at the top of README.md and of the crates.io, npm, and
    PyPI READMEs, stating the purpose in plain language and linking to the above.
    On the registry pages that link is pinned to the release tag, for the same
    reason the logo already was: a published README cannot be edited, and a
    main-relative link in one will eventually point at something else.

Changed

  • tools/sync-version.sh now also rewrites the tag-pinned README-FOR-DUMMIES.md
    link in every published README, so the release checklist stays one edit.

loopsmith v0.1.3

Choose a tag to compare

@github-actions github-actions released this 17 Aug 20:44

The release where Windows stopped being a badge and started being a platform.

Fixed

  • which() found nothing at all on Windows. It joined the bare command name
    onto each PATH entry, and executables there are git.exe — so nothing ever
    matched and it returned None for every command on the machine. Every caller
    then faithfully reported the falsehood it was handed: doctor listed every tool
    as absent, Platform::detect found no scheduler, and worktree isolation
    degraded to the shared directory with "git not on PATH" on a runner where
    actions/checkout had just used C:\Program Files\Git\bin\git.exe. The
    schtasks support added in 0.1.0 was dead on arrival for the same reason,
    because which("schtasks") could never succeed. Roughly a third of the Windows
    claim was decoration.

    Each PATHEXT suffix is now tried as well as the bare name, honouring the
    variable rather than hardcoding a list — it is how a machine says .ps1 counts
    as a command. The suffix is appended, not set as the extension, so check.sh
    can become check.sh.exe where set_extension would have produced check.exe.

    Command::new("git") was never affected: Windows' CreateProcess appends the
    extension itself. Only loopsmith's own lookups were broken.

  • The generated .cmd launchers reported success when they had failed.
    setlocal saves the current errorlevel and the implicit endlocal at the end
    of a batch file restores it, so a bare exit /b 127 inside a setlocal scope
    exits 0. A loop whose pinned binary had moved printed "loopsmith is not at
    … and not on PATH" and then exited successfully — the precise silent failure
    the exit code exists to prevent, and one a scheduled job would never surface.
    resume.cmd's "no run id given" exit 2 had the same defect, as did the export
    launcher.

    endlocal & exit /b <code> fixes it on a top-level line but not inside a
    nested if ( … ) block, which is where the broken one lived — so each launcher
    now has exactly one exit /b, on its last line, reached by every path via
    goto. They also enable delayed expansion and capture with !ERRORLEVEL!,
    because a parenthesised block is parsed before it runs and %ERRORLEVEL% inside
    one reads the value from before the command. A test asserts the single exit,
    the label, and the absence of parse-time capture.

  • loopsmith new closed by telling every user to run run.sh, including on
    Windows where cmd.exe cannot execute it and run.cmd was sitting beside it.
    It now names the launcher the host can run, and prints set rather than
    export for the API-key line there.

Changed

  • Every published package now carries a README that stands on its own. The
    crates.io, npm, and PyPI pages previously said little and pointed at the
    repository; someone arriving from a registry had to leave to learn what the tool
    was. Each now explains the design, shows a real config, lists the subcommands,
    and covers install, providers, scheduling, and platform behaviour in the idiom
    of that registry. The eight library crates had no README at all and rendered
    as blank pages; each now explains its own role and where it sits.
  • CI no longer downloads a third-party toolchain or cache action. rustup is
    preinstalled on GitHub-hosted runners, and eight consecutive runs lost a leg to
    codeload 429/502/503 while fetching an action — before a line of loopsmith
    compiled. continue-on-error could not save it either: actions are downloaded
    during "Set up job", before the step that was allowed to fail ever runs. A cold
    build takes about two minutes, so the cache was saving less than the flakiness
    cost.
  • Two tests that invoked ./run.sh and ./resume.sh directly now pick the
    launcher the host can execute. Gating them with #[cfg(unix)] would have made
    the suite green while leaving the .cmd launchers unexercised on the only
    platform that runs them.

loopsmith v0.1.2

Choose a tag to compare

@github-actions github-actions released this 17 Aug 15:26

Fixed

  • The PyPI launcher exec-looped instead of running. ensure_binary() had a
    "reuse a loopsmith already on PATH" shortcut, and pip installs this package's
    console script as loopsmith on PATH — so shutil.which("loopsmith") found the
    very script that was running and execv re-entered it forever. The symptom was
    the worst kind: no output, no error, no traceback, just a command that never
    returns. The shortcut is gone, because telling our own console script apart from
    a cargo-installed binary means comparing argv[0], the interpreter's script
    directory, and the symlinks between — all to save one download that happens once
    per version. __main__ also refuses outright if the binary it resolved is the
    launcher itself, since that failure mode has no useful symptom to debug.

Only the PyPI wrapper was affected. loopsmith-cli 0.1.1 on PyPI is unusable;
0.1.2 is the version to install. The npm wrapper resolves a fixed path beside
itself and never had this bug.

Everything else is byte-for-byte 0.1.1. The version is bumped across all four
registries rather than only on PyPI, so one number means one thing everywhere.

loopsmith v0.1.1

Choose a tag to compare

@github-actions github-actions released this 17 Aug 15:00

The first release the CI matrix ever ran against, and it found something on its
first attempt — which is the entire argument for having added it.

Fixed

  • schedule --install no longer lies on Linux and Windows. The flag was
    threaded to the launchd branch and dropped by the other two, so
    loopsmith schedule loop.yaml --install on Linux did exactly what it did
    without the flag, silently, leaving the user believing a schedule had been
    registered. It now explains why there is nothing to write: only launchd has a
    per-job file loopsmith can add without touching entries it does not own. A
    crontab is one file per user with no drop-in directory, and Task Scheduler
    keeps its jobs in a database reached only through schtasks. In both cases the
    next step was always "run the command above yourself".
  • A test asserted the launchd wording on every host, so it had been passing by
    describing macOS. It now asserts that whichever scheduler path ran names the
    next step, and a new test covers the no-op --install from both sides.

Everything else in 0.1.0 is unchanged. 0.1.0's binaries remain published; this is
the version to install.

loopsmith v0.1.0

Choose a tag to compare

@github-actions github-actions released this 17 Aug 14:42

First release. The design in one line: goal_satisfied is written by a
deterministic Rust gate and by nothing else, and the gate can revoke.

The runtime

  • A–J config model in YAML or Markdown, with validate refusing to run until
    every pre_execution step is marked done. That refusal is the feature: a
    process you cannot describe in checkable terms should not be automated.
  • Deterministic verification gate. Detectors are file_exists, regex,
    script, and composites. The gate promotes and revokes — delete a required
    artifact and a satisfied goal flips back.
  • DAG scheduling with critical path and Amdahl-derived concurrency, and
    phases that open one at a time.
  • Four layered stop gates — iterations, per-node revisions, wall clock, and
    no-progress — all evaluated every iteration. Their accounting lives in the
    checkpoint, so a resume cannot refund a spent revision budget.
  • Shared memory on sled: episodes, goal state, an append-only ledger,
    checkpoints, per-goal scratchpads, and skill trials.
  • Provider routing for Claude Code, Ollama, Grok, OpenAI, Gemini, Hermes,
    MCP, and any BYOK command, with a per-tier cascade and enforced judge
    independence. requires_env names the variables a provider needs; loopsmith
    checks only that they exist and never reads their values, so a key cannot
    reach a prompt, a log, or the ledger.
  • Git worktree isolation per builder, degrading to the shared directory —
    and saying so — outside a repository or without git.
  • A local stdio MCP server exposing memory, gate, and graph to any MCP
    client.
  • Sub-agent acquisition ordered installed → marketplace → generate, with a
    star floor, credential-shaped-name refusal, and quarantine until a human
    promotes.

Cross-platform

  • Linux, macOS, and Windows, verified by a CI matrix rather than asserted.
  • Nothing is decided at build time. The userland is probed by asking sed
    for a version, because Homebrew's coreutils puts GNU tools ahead of BSD ones
    on a Mac and the operating system therefore does not imply the answer. The
    scheduler is whichever candidate is on PATH: launchctl then crontab on
    macOS, crontab then systemctl on Linux and BSD, schtasks then crontab
    on Windows.
  • Every new loop gets both launcher flavoursrun.sh/resume.sh and
    run.cmd/resume.cmd — on every host, because a loop directory outlives the
    machine that produced it. Every .sh is POSIX, since macOS ships bash 3.2.
  • scripts/compat.sh travels with each loop: sed_i, stat_size,
    stat_mtime, readlink_f, sha256, require, need_bash. require and
    need_bash exit 2, not 1 — a detector's exit code is its verdict, and
    "cannot run the check" is not "the check failed".
  • loopsmith doctor reports the host and what it stops you doing.
  • Home directory resolution honours USERPROFILE and HOMEDRIVE+HOMEPATH,
    not just HOME.
  • Universal installers: install.sh for Linux/macOS/BSD, install.bat +
    installers/deps.ps1 for Windows.

Packaging

  • crates.io: loopsmith plus the eight libraries behind it.
  • npm: @bitphill/loopsmith. PyPI: loopsmith-cli. Both names differ from the
    command because loopsmith was already registered on those registries by
    unrelated projects; the installed binary is loopsmith either way.
  • Homebrew: the bitphill/loopsmith tap.

Decisions worth knowing about

Two things look unfinished and are not:

  • --install writes a scheduler definition and stops. It does not run
    launchctl load -w or schtasks /Create. Registering a scheduled job is a
    persistent, user-visible change to someone's machine, so it stays their call.
  • Cron expressions are evaluated in UTC. Deriving a correct local offset in
    a multithreaded process is unsound on Unix without care, and a scheduler that
    is quietly an hour off twice a year is worse than one that is honestly in UTC.
    schedule says so in its output. For a cadence that does not care, use an
    interval trigger.

And one default that is deliberately impatient: the starter ollama provider
times out at 120 seconds. ollama run pulls a missing model, a pull is
indistinguishable from slow generation from outside the process, and the point of
a cheap tier is to be abandoned quickly. Run ollama pull <model> first.