Releases: bitphill/loopsmith
Release list
loopsmith v0.3.1
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--guidedruns 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
onPATH, 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,⌘Kswitches 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 ofguided/sections.rs
rather than a second implementation of it. - Sections are asked in
Fixed
- The step panel never unmounted the step you had just left.
MorphPanel
drove its transition withAnimatePresence, whose exit phase never completes
under React 19's StrictMode with the pinnedmotionrelease. 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-spacingowning 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/distis 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.mdsaid 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
Added
-
loopsmith --guided(alsoloopsmith 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 thatnewhands you as a file and--webpaints 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 sameloopsmith validatea hand-written file would.- Agent CLIs already on
PATHare 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:quitwork at every prompt;:quitoffers
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-featuresbuild: the provider catalog moved to
src/catalog.rs(plain data, no async) so the wizard needs nothing from the
webfeature, and detection is a synchronousPATHscan.
New dependency
ctrlchandles Ctrl-C identically across macOS, Linux, and
Windows. - Agent CLIs already on
loopsmith v0.2.2
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_TOKENandPYPI_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-providerfailed becauseloopsmith-utilhad 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. -
authoris set on all three packages — missing entirely from npm, and a
bare handle on crates.io.
loopsmith v0.2.1
Patch. One user-visible bug in loopsmith web, no behaviour change anywhere else.
Fixed
-
--portno 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
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 to127.0.0.1is same-origin to the
browser, so no CORS check ever runs and the request arrives looking ordinary.Verified exploitable before the fix:
Host: evil.comreturned 200 on every
endpoint, and a cross-originPOST /api/jobsexecuted. That reachability
meantPOST /api/secrets/revealcould read back every API key in the shell
profile, andPOST /api/secretscould 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 theHostheader, because the browser sets it from the URL. Requests
not addressed to a loopback name are refused, as are state-changing requests
carrying a foreignOrigin. 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, includingPATH,LD_PRELOAD,
DYLD_INSERT_LIBRARIES,NODE_OPTIONSandGIT_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/jobshad 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_secretswas the
worst: onesecurityspawn 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
dropnor process exit makes instantaneous — so opening a store microseconds
after the previous holder let go returnedWouldBlockand 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.opennow 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.
-pis not a Hermes
option;-z/--oneshotis, and its own description is exactly what a loop
wants: one prompt, only the final response on stdout, no banner or spinner. -
llmwould 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,
codexandllmare 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 --gitinitialises a repository, with one commit, in the
new directory. This is what makesisolated: trueisolate: 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, becausegit worktree addresolves 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
releaseanddistbuild profiles.cargo installand Homebrew
build on the user's machine while they wait, soreleasestays at thin LTO.
The published npm and PyPI binaries are built once in CI, where nobody is
waiting, so the release workflow now usesdist— 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.panicstays atunwindin both. The usual argument forabortis 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; underabortone 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;
--webcombined with a subcommand is refused rather than silently resolved.
Serves127.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.mdwas 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 spawnscurrent_exe()rather than calling
the crates, so the browser cannot drift from the CLI and cannot do anything
loopsmith --helpdoes 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
PATHwith their versions, Ollama models
viaollama 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 factsdoctorreports.
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.zshrcrather than the.profilezsh never
reads. Only the key name ever reaches a config, viarequires_env. -
The thirteen examples ship inside the binary and load in one click.
tools/sync-examples.shcopiesconfig/examples/*.yamlinto the crate, since
include_str!cannot reach above the package root andconfig/is excluded
from the published tarball. A test fails when the copies have drifted, so a
stale example is caught bycargo testrather than by a user. -
The logo in the UI — header, firs...
loopsmith v0.1.4
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>.mdwritesloop.mdand points
run.shat it, so the reader never has a second config file to be confused by.
Scheduling is taught asintervalseconds; 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_existsandjudge. It also tells the reader to delete every
type: scriptcheck, 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.mdand 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.shnow also rewrites the tag-pinnedREADME-FOR-DUMMIES.md
link in every published README, so the release checklist stays one edit.
loopsmith v0.1.3
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 eachPATHentry, and executables there aregit.exe— so nothing ever
matched and it returnedNonefor every command on the machine. Every caller
then faithfully reported the falsehood it was handed:doctorlisted every tool
as absent,Platform::detectfound no scheduler, and worktree isolation
degraded to the shared directory with "git not on PATH" on a runner where
actions/checkouthad just usedC:\Program Files\Git\bin\git.exe. The
schtaskssupport added in 0.1.0 was dead on arrival for the same reason,
becausewhich("schtasks")could never succeed. Roughly a third of the Windows
claim was decoration.Each
PATHEXTsuffix is now tried as well as the bare name, honouring the
variable rather than hardcoding a list — it is how a machine says.ps1counts
as a command. The suffix is appended, not set as the extension, socheck.sh
can becomecheck.sh.exewhereset_extensionwould have producedcheck.exe.Command::new("git")was never affected: Windows'CreateProcessappends the
extension itself. Only loopsmith's own lookups were broken. -
The generated
.cmdlaunchers reported success when they had failed.
setlocalsaves the current errorlevel and the implicitendlocalat the end
of a batch file restores it, so a bareexit /b 127inside asetlocalscope
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
nestedif ( … )block, which is where the broken one lived — so each launcher
now has exactly oneexit /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 newclosed by telling every user to runrun.sh, including on
Windows wherecmd.execannot execute it andrun.cmdwas sitting beside it.
It now names the launcher the host can run, and printssetrather than
exportfor 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.
rustupis
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-errorcould 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.shand./resume.shdirectly now pick the
launcher the host can execute. Gating them with#[cfg(unix)]would have made
the suite green while leaving the.cmdlaunchers unexercised on the only
platform that runs them.
loopsmith v0.1.2
Fixed
- The PyPI launcher exec-looped instead of running.
ensure_binary()had a
"reuse aloopsmithalready on PATH" shortcut, and pip installs this package's
console script asloopsmithon PATH — soshutil.which("loopsmith")found the
very script that was running andexecvre-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
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 --installno 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 --installon 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 throughschtasks. 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--installfrom 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
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
validaterefusing to run until
everypre_executionstep 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_envnames 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 onPATH:launchctlthencrontabon
macOS,crontabthensystemctlon Linux and BSD,schtasksthencrontab
on Windows. - Every new loop gets both launcher flavours —
run.sh/resume.shand
run.cmd/resume.cmd— on every host, because a loop directory outlives the
machine that produced it. Every.shis POSIX, since macOS ships bash 3.2. scripts/compat.shtravels with each loop:sed_i,stat_size,
stat_mtime,readlink_f,sha256,require,need_bash.requireand
need_bashexit 2, not 1 — a detector's exit code is its verdict, and
"cannot run the check" is not "the check failed".loopsmith doctorreports the host and what it stops you doing.- Home directory resolution honours
USERPROFILEandHOMEDRIVE+HOMEPATH,
not justHOME. - Universal installers:
install.shfor Linux/macOS/BSD,install.bat+
installers/deps.ps1for Windows.
Packaging
- crates.io:
loopsmithplus the eight libraries behind it. - npm:
@bitphill/loopsmith. PyPI:loopsmith-cli. Both names differ from the
command becauseloopsmithwas already registered on those registries by
unrelated projects; the installed binary isloopsmitheither way. - Homebrew: the
bitphill/loopsmithtap.
Decisions worth knowing about
Two things look unfinished and are not:
--installwrites a scheduler definition and stops. It does not run
launchctl load -worschtasks /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.
schedulesays so in its output. For a cadence that does not care, use an
intervaltrigger.
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.