You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fettler ΓÇö file,
search and edit tools for a model and a script, with no shell in between.
The project is Fettler; the command is fettle. It performs the operations
an assistant actually carries out on a source tree ΓÇö find, search, read,
write, edit, replace, move, copy, delete, extract, and run a declared task ΓÇö
as one program with a command line and an MCP front end over one dispatcher.
It ships as a self-contained single-file download per OS, attached to this
release beside the .vsix and sparky. It needs no .NET on the target
machine and none of Shoddy to run.
A boundary you declare, and nothing outside it exists.
A path outside every declared tree is refused with exit 4 ΓÇö identically
whether or not it is there, so a refusal cannot be used to ask what is on a
disk. There is no working directory, no path that climbs out, and no shell
to reach past it with. So an assistant cannot drift into a sibling checkout,
a vendored copy or an archived branch and answer from it fluently. Damage is
the obvious half; grounding is the half that bites quietly.
Seven permissions, per tree and per folder.list read create update rename delete execute, stated in a .fettler.json a person writes. A scope replaces what its tree grants rather than adding to it, so it can take a
permission away ΓÇö which is what makes "may reorganise, may never destroy"
sayable, and what makes a folder inside a readable tree genuinely invisible. execute is never a default anywhere, including the tree the configuration
sits in. See the boundary.
The tool cannot write the files that say what it may do..fettler.json
and .fettler.local.json are refused by every write path at every
permission level, with their own outcome governed and exit code 11.
Without it the permission model is decoration: the escape was two commands
long ΓÇö write a task declaration, then run what you just wrote.
Nor the files that say what the assistant may do..mcp.json, .claude/settings.json, .claude/settings.local.json and .vscode/mcp.json refuse on the same terms. No boundary has to be breached
to reach one: a project's assistant configuration lives in the project, so
the file governing the assistant and a file the assistant may write were the
same file. A model that can edit its own deny list has no deny list. Matched
on the last two segments, so a bare settings.json and .vscode/settings.json stay ordinary; CLAUDE.md is left out because it
persuades rather than permits. setup still writes all of them.
A write that would introduce a credential is refused ΓÇö outcome credential, exit 12. It judges the diff, not the file: only a secret
absent from the previous content stops a write, so a config that already
holds a key stays editable, which is the difference between a rule people
keep and a rule people switch off. Two tiers ΓÇö issued credentials whose
shape their issuer fixed, and a secret-shaped name assigned a long,
high-entropy, whitespace-free value that is not a reference, so an empty
password and a value naming an environment variable both pass. The
refusal never repeats what it found: a line number and a detector name
only, because a message naming the secret writes it into the log and the
transcript. --allow-credential is offered by the command line and by
nothing else. It is a safety net, not a boundary ΓÇö base64 the value or
split the string and every rule is defeated. It stops the ordinary
accident, not an adversary.
fettle doctor answers, in one screen, whether Fettler is registered
with each client at each level, whether the binary it names can be
launched, and what on this machine still lets an assistant go round the
boundary ΓÇö a pre-approved sed -i, an allow on a built-in editor, an
instructions file that has never heard of Fettler. It writes nothing. fettle setup writes the wiring; it is the one verb a model is never
offered, because it names the command that launches the server. Setting it up.
It reads more than source. Notebooks as cells, PDFs as text per page,
images as facts plus a native image part, and archives as a manifest ΓÇö --member NAME reads one entry out without unpacking anything. --tail N
is the end of a log without needing its length. extract unpacks into a
declared tree, refused whole or not at all, with zip slip refused by
construction rather than by a check bolted on.
One version, stamped on every binary the release builds. The mill's
banner, sparky's serverInfo and fettle --version now answer the same
number, from one place. Before this, nothing stamped a version at all.
Changed
The gate covers all four lanes, not one of them.scripts/shoddy.ps1 gate now proves core, fettler, the MCP host and the MAUI host, plus the
harness's own self-test, both shipped archives driven from outside the
repository, and the headless floor. Three of those four suites used to run
nowhere but a workflow, and every one of those workflows is path-filtered
ΓÇö so a change outside their paths triggered none of them. Directory.Build.props, which sets the version every one of those binaries
reports, matched no filter at all. The workflows remain as the backstop; the
gate is no longer a claim about one quarter of the repo.
A receipt names the tree it was earned against, not the commit. gate --resume used to key on the SHA alone, and on a dirty tree it skipped
all 72 steps and printed a pass in a tenth of a second, describing a tree
from before the morning's work. publish was never at risk ΓÇö it demands a
clean tree and a receipt for the exact commit it will tag ΓÇö but a maintainer
reading that pass was being told something false. Receipts now key on the
commit plus a digest of everything uncommitted.
Fixed
Debug and Release stopped sharing one mill's bin/. Every
configuration wove to the same mills/<name>/bin DLL, so a Release build
starting while the previous step's test host was still exiting failed its
final move with "Access to the path is denied". Neither existing guard
reached it: MillGate serializes writers across processes, and
weave-beside-then-rename protects a compiler reading the previous weave ΓÇö
but the holder here had the assembly loaded, and Windows holds a loaded
assembly against rename until its process exits. No lock can fix that, so
the configurations stop sharing the path instead. Set ShoddyBinScope on a
lane that builds one mill in two configurations at once; it is empty by
default, so nothing else changes. See hosting.
The Fettler page claimed an atomicity the code does not promise. A batch
resolves all-or-nothing ΓÇö every file read, hash-checked and every
substitution located before the first byte moves ΓÇö but the writing itself
can still fail halfway on a full disk or a file another process holds open.
The page now says so, and says what you get instead: the file it stopped at
and how many had already been written, which identifies the survivors
exactly rather than leaving them to be guessed.
mills/pac-vt100/mill.manifest had lost julian. A generated file
describing the mill as it used to be, and the only thing in the repository
that noticed was the MAUI host lane's build ΓÇö which nothing local ran and
which CI only runs when hosts/maui/** changes. A file in mills/ was
breaking a lane in hosts/, connected by nothing that would ever fire. verify-manifests.js now checks every manifest that exists on every gate
run.