Releases: shoddymills/shoddy
Release list
v2.6.0
Changed
- Fettler has moved to its own repository. The file, search and edit
tools for AI assistants now live — and release — at
shoddy-fettler, with
documentation at
shoddymills.github.io/shoddy-fettler.
This repository no longer shipsfettle; every Fettler link in the docs
points at the new home. - Releases are made differently now. The tag is the version: the
Release workflow stamps the number from the tag, so there is no
version-bump commit and no release branch — a release is cut frommain
after a pull request CI has proven. The maintainer procedure is
WORKFLOW.md,
the reasoning
RELEASING.md. ./build.ps1 check(and thebuild.shtwin) is a new verb running the
seven fast verify gates — docs, errors, permissions, host-blind, suites,
twins, lanes — in seconds;./build.ps1 testremains the whole suite.
The Toolchain page
documents the full verb set../scripts/shoddy-status.ps1says where a piece of work has got to — the
branch and whether it is clean, how far it sits frommain, whether it
has been pushed, the state of its pull request, CI's verdict on the
commit, whether the release notes for a version exist and are committed,
and whether that tag is still free — then names the single command that
comes next. It is the one script here that reports rather than acts: the
only thing it writes is the remote-tracking refs it fetches, and it
always exits 0, because a status tool that fails on incomplete work
becomes a gate../scripts/shoddy-status.shis the twin.
Fixed
- The
julian,ephemerisandclockcore suites ran only in the
retired release gate, never in./build.ps1 test. The suite roster is
now a single copy (scripts/suites.mjs) read by the build, the display
runner and the verify gate alike, so a suite can no longer fall between
them. - New
.ps1scripts were committed without their execute bit.
scripts/shoddy-commit.*staged the bit on new.shfiles only, while
verify-permissions.jsapplies it to every tracked file that opens
#!— so a new PowerShell script fell straight through the window the
.shcase exists to close. Six had, and are corrected here. Both twins
now test the shebang, which is the rule the gate itself applies.
v2.5.1
The disclosure screen's category words have changed, and the old ones are
refused. If any .fettler.json or .fettler.local.json on your machine
names phi, pii or sci in a screen, it will stop working when you
install this release. Two of the three new words are a straight rename; one
of them splits in two.
| Was | Write instead |
|---|---|
pii |
identifiers |
sci |
scientific |
phi |
identifiers for the labelled patterns, clinical for model screening of clinical text — it was doing duty for both |
The old words are refused rather than translated, when the configuration
is read, with a message naming the replacement:
'phi' is no longer a screening category; the labelled identifier patterns are
'identifiers', and clinical-model screening is 'clinical'
Translating them silently would have kept them alive in configurations
indefinitely, still promising what they always over-promised — which is the
whole reason for the change.
Why the words changed
phi and pii claimed more than the screen delivers. A category called
phi reads as "protected health information is covered here". What its
pattern tier actually found was a record number under an MRN: label and a
date under a DOB: label. Patient names, addresses, conditions and the
clinical prose around them were never matched by anything unless you had
installed a model — and nothing said so.
The honest sentence, which the documentation now leads with: out of the
box the screen catches structured identifiers — a social security number,
a Luhn-valid card, a formatted phone number, an email address, a labelled
record number and a labelled date of birth. Names, addresses, conditions and
free text are caught only by a model you install, and the manifest plus
roots name exactly which model that is.
The new words say which is which. identifiers is the pattern tier and
takes no model, ever. clinical, legal and scientific are model tiers
and screen nothing at all until you install one.
Added
-
fettle rootsnames the model doing the judging, and never hides it.
Where a grant screens a model-backed category,rootsnow reports that
category on its own line: the checkpoint and revision read from the
model'smanifest.json, orno model installed - reads here will refuse
where the directory holds none.records /work/records can: list read screen: identifiers clinical clinical: obi/deid_roberta_i2b2 @ a1b2c3dWith no
"models"directory declared at all, one line says so rather than
letting the category words imply a protection that is not running:no "models" directory is declared, so clinical, legal, scientific screen nothing here - only the identifier patterns runThe JSON answer carries the same facts under
screening, because the MCP
front end never sees the text one — and that is the front end where a
model is the thing reading the answer. Manifest reading is read-only and
tolerant: an unreadable or incomplete manifest reports as no model rather
than taking the whole answer away, sincerootsis very often the call
somebody makes because something is already wrong. -
scripts/shoddy-branch.*replacesscripts/shoddy-feature.*, and adds
abugverb.bug NAMENNcutsbug/<origin-feature>NNoff an
up-to-datemain, so a fix carries the name of the feature it belongs to
and its number:bug/pudsey01is the first fix to work that shipped from
feature/pudsey. The two digits are not optional —bug/pudseyrefuses.
shipruns fromfeature/*andbug/*alike and merges each the same
way. The script was renamed becausefeaturewas never the only thing it
cut.
Changed
-
A pattern-only screen never starts the sidecar. A scope screening
identifiersand nothing else has no question to put to a model, so
burler is not consulted, not started, and its absence is not a refusal.
Previously the category words made this hard to reason about; now it
follows from them. A tree screening onlyidentifiersnever needs a
models directory at all. -
Every pattern finding reports as
identifiers. A refusal that used to
read2 in phinow reads2 in identifiers. The count and the category
are still all it says — never the text it found. -
The documentation says what the screen catches, rather than implying
more. Screening
gains a column naming the model family behind each category and stating
that the manifest androotsname the exact checkpoint; the
four-categories tables, the workedrootsoutput, the refusal examples
and every configuration snippet carry the new words. The claim that the
first tier "already carries most of" the personal-identifier category is
gone — it was the clearest form of the over-promise. -
The workstation diagram draws all eleven denied built-ins.
Monitor,
BashOutputandTaskOutputwere added to the deny list in v2.5.0 and
never drawn.Monitoris a shell — it runs what it is handed in the same
environmentBashdoes — and the two output readers start nothing and
write nothing but hand back the output of work already done, bytes that
reached the model without passing the tree boundary, the secret scan or
the disclosure screen. The figure'saria-labelnames them too.
Fixed
- A worked example on the screening page tripped the screen it
documents. The page carried a literal labelled date of birth as sample
text, so thedate-of-birthdetector matched it and any screened tool
reading the page was refused — the documentation for the feature was
unreadable to anything the feature was switched on for. The example now
describes the shape instead of spelling it.
v2.5.0
Fettler can now refuse to hand back regulated personal data.
v2.4.0 taught fettle to read Excel and Word and to search inside those
and PDFs — which is to say it taught it to open exactly the formats
health, consumer-report and education records live in. Everything it
returns lands in an assistant's context, and from there in a transcript,
a log, and whatever ships those onward. Nothing inspected that payload on
its way out.
Now a tree can declare a disclosure screen, and any detection in a
screened category refuses the whole response. It is off everywhere until
a tree asks for it, and most trees never will.
Added
-
A tree or a scope may declare
"screen".truescreens all four
categories —phi,pii,legal,sci. A bare list includes and a
-list excludes from the full set, so["phi", "legal"]screens
those two and["-sci"]screens everything else. Mixing the forms in
one list is refused when the file is read, naming the ambiguity:
["phi", "-sci"]reads either as "phi only" or as "everything but
sci", and those differ by two whole categories, so a rule picking one
would silently withhold a screen from somebody who believed they had
asked for it. A misspelt category is refused for the sharper version of
the same reason — it would quietly subtract one. -
A scope's
screenreplaces the tree's, exactly as itscandoes,
which is what lets a scope carve out an anonymised subdirectory. A
scope that says nothing about it inherits, because every scope
written before this existed says nothing, and reading that silence as
"screen nothing here" would punch a hole in the first tree anybody
switched it on for. -
It judges the disclosure, not the file. A document may hold a
record number on page 40; a read of page 2 that discloses none is
served. This is the same choiceSecretsmakes in judging what a write
adds rather than what a file contains, and for the same reason —
judging the whole file locks the very trees people most need help in,
and the way round that is to turn the screen off. Asearchis judged
as the hit records it is about to return, per file, including whatever
--contextbrings with them. -
Tier one needs nothing installed. Structural detectors run in
process with no model and no new dependency: a social security number
with the issuing authority's own invalid ranges excluded, a card that
passes Luhn, a phone number, an email address, and — under their own
labels, because no issuer agrees on a shape — a medical record number
and a date of birth. They are narrow deliberately. Nine bare digits are
an order number far more often than an identifier, and a check that
cries wolf is a check somebody switches off. -
burler, a second executable, hosts the models. The trade's word
for whoever picks the faults out of finished cloth before it leaves the
mill. It is a separate program for one reason: BERT inference needs
ONNX Runtime, which ships native per-RID binaries, and Fettler's
package allowlist admits only pure managed assemblies so thatfettle
can publish self-contained and single-file. The allowlist did not
change, and the test that enforces it did not change. Neither project
references the other in either direction — the line-delimited JSON wire
is the whole contract, and a protocol test on each side asserts it
independently, in theverify-twinsspirit: twins proven equivalent by
test rather than by sharing. -
The sidecar is lazy, warm and bounded. It starts on the first
screened disclosure, is held so a warm answer costs milliseconds rather
than a model load, and is killed once it has been idle — a tool sitting
in an editor all day should not hold hundreds of megabytes for a
document somebody read at breakfast. -
A new
screenedoutcome and exit code 13, besidecredentialand
12. They are mirror images — one is a write going in, the other a
payload coming out — and a script branching on "a secret was involved"
would take entirely the wrong action for each. -
rootsreports the screen where one is on, because a boundary a
caller cannot read is one they can only learn by being refused. -
doctornow reports what the deny list cannot see. The deny list
closes tool names, and two things sit outside any list of names.
2.17 names every other MCP server registered besidefettler—
their tools are not calledRead, so nothingsetupwrites says
anything about what they may reach. It reads the machine in front of
it, so a server installed tomorrow is reported tomorrow, and it names
rather than judges: most of them are wanted. 2.18 reports any tool
the configuration names that thisfettle's inventory does not
know — because check 2.8 saying nothing about a tool reads exactly
like a clean bill of health, and is not one. Every report now prints
the date the inventory was drawn, so a list that has fallen behind
the client is visible instead of silently passing.
Changed
-
The exclusion list gains three names —
Monitor,BashOutputand
TaskOutput— sosetupwrites eleven denies rather than eight.
Monitoris a shell: its own description says it runs the command it
is handed in the same shell environmentBashdoes, so denyingBash
and leaving it open denied the word and not the thing.BashOutputand
TaskOutputare readers, and were missed for being neither an editor
nor a shell — they start nothing and write nothing, but they hand back
the output of work already done, and those bytes reach the model
without passing the tree boundary, the secret scan or the disclosure
screen.TaskOutputsettles it by naming its own alternative:Read
on the task's output file, a tool already on the list. -
build.ps1 test/build.sh testrun both test projects.burler
has its own, so that each side of the pipe asserts the wire without
being able to see the other's types — a shared DTO assembly would make
the two agree by construction, and then the one day they should
disagree nothing would say so. -
build.ps1 publish/build.sh publishproduce two archives per
OS.burleris optional and only wanted by somebody who has switched
the screen on, so folding it intofettle's archive would make every
download pay for ONNX Runtime to get a feature most trees never use.
fettlelooks for it beside itself, so the two unpack into one
directory when both are wanted.
Notes
-
No model weights are distributed by this project, in any archive.
Four quantised models run about 400 MB against afettledownload
measured in single-digit MB. A person fetches the checkpoints they want
and names the directory as"models"— usually in
.fettler.local.json, since where they sit is a fact about a disk
rather than about a project. Each carries amanifest.jsonrecording
the checkpoint, its revision, its licence and where it came from, and
burlerrefuses to start a category without one.That is a licensing position as much as an engineering one. Several
clinical de-identification checkpoints derive from data-use agreements
restricting redistribution, and some capable legal-domain checkpoints
are ShareAlike. Redistributing none of them means no obligation
attaching to their redistribution is incurred. -
Naming that directory is the act that makes the second tier
load-bearing. Before it is named, tier one is the whole screen and a
clean payload is served — nobody has claimed a model is installed, so
there is no second tier to have failed. Once it is named, a screened
category whose model is missing refuses. -
Everything that can go wrong refuses. A sidecar that will not
start, a model that will not load, a malformed answer and an inference
that outran its clock all deny the disclosure and say which. An image
in a screened scope is refused too: nothing here can read one, and a
photographed discharge summary is precisely what the screen exists to
stop. Serving content because the check broke is the one outcome this
must never produce. -
The refusal never repeats what it found. A category and a count. A
refusal naming the patient it found would write that name into the log
and the transcript — the exact harm the screen exists to prevent,
delivered by the screen itself. Where the model found each entity
crosses the pipe and stops there.
Documentation
-
Screening
is a new page: deciding whether you need it, switching on the tier that
needs nothing installed, adding the sidecar, choosing and preparing
models, and proving a refusal refuses. It is the procedure; the
reference behind it is below. -
The disclosure screen
is a new section on the Fettler reference page, with the configuration
forms, what each tier catches, every refusal it can produce, and what
the screen deliberately does not do. The setup page gains a short
section on switching it on, and the exit-code table gains 13. -
Two limits are written down rather than left to be assumed.
FCRA and FERPA are not covered categories — a clinical model knows
clinical text and a scientific one knows scientific text; neither knows
a student transcript or a credit report, and only the structured
identifiers in those regimes are caught. And this is a safety net,
not a boundary: models miss entities, the patterns miss anything
written a way they do not expect, and a number split across a line
break defeats every one of them. A clean verdict is evidence of absence
and never a certificate of it. Anyone who believes a screened tree
cannot leak has been misled, and that belief is the actual hazard.
v2.4.0
A Fettler release, and a pass over the repository's own scripts.
fettle reads Excel and Word now, and searches inside them - along with the
PDFs it could already open. Not being able to read a workbook or a document
meant converting one by hand before an assistant was allowed to look at it,
which is a chore rather than a workflow.
And search can see inside all three. It could not see inside a PDF at all
before this, which made for the worst kind of gap: the file opened by name and
was invisible to every search, so a pattern that was plainly in the document
came back as a calm, confident zero.
Added
-
Spreadsheets are read.
.xlsxand.xlsmcome back a line per row,
sheet by sheet, in the order the workbook lists its sheets rather than the
order the files happen to sit in. Formulas are rendered as well as the
values they last worked out to - a search forVLOOKUPis a question
about how a sheet works and would find nothing at all in the cached
results. Dates are rendered as dates, both calendars and the 1900 leap
year Excel invented, because a workbook stores one as a day count and a
workbook full of unsearchable numbers is not much better than a shut one. -
Word documents are read.
.docxand.docmcome back as their
paragraphs and tables, with headings kept as headings - and the headings are
what a hit is cited under, since Word does not store where its pages break.
Tables render a row at a time, footnotes and endnotes are included because
they carry real content, and text removed by a tracked change is left out:
nobody reading the document can see it, so nothing should find it. -
Neither adds a dependency. Both formats are a zip full of XML, and the
zip and XML readers were already here in the shared framework. The one
packagefettlebundles is still PdfPig, still only for PDF. -
Documents are searched, not skipped.
searchrenders a document to text
and searches that, like any other file. Hits carry the page, sheet or
heading they were found under, so a result can be turned back into somewhere
to look. Nothing is unpacked to disk. -
A reader mechanism, so the list can grow. A document type is one small
class saying which extensions it claims and how to turn bytes into text plus
landmarks;read,searchand the kind reported for a file all follow from
that one registration. It is a table compiled in, not a plugin loader:
running a declared, named command already needs an explicitexecute
grant, so loading unnamed code in process would be a hole straight through
the boundary the tool exists to draw. -
write,editandreplacerefuse a document by name. What comes back
for one is a rendering rather than the file, with the sheets, the formulas
and the structure left behind in the making of it. Writing that text back
would not edit the document, it would replace it with a fraction of itself
still wearing a name that says it is a document. The check sits in the one
function every verb that writes text passes through, so there is no verb to
forget;extractis deliberately not caught by it, since that writes a
document's own bytes. -
Search says when it did not look. An answer reports how many documents
it left shut, in the text form and asdocuments_skippedin JSON. A file
that was never opened is not a file with nothing in it, and the two used to
be spelled the same way.--no-documents(no_documentsover MCP) asks for
the old behaviour deliberately; documents are searched by default. Anything
over 32 MB is skipped and counted rather than rendered. -
A declared task's command line can name a value the configuration
supplies. A task still takes no arguments from its caller - that is the
whole point of one - but writing{like-this}in itsrunline fills it
from areplacementsblock, and an undeclared name is refused rather than
passed through. The value can live in.fettler.local.json, which is not
checked in, so the committed file declares the task every checkout shares
and the overlay supplies what changes per person and per moment. A value
containing spaces stays one argument: the command line is split before it
is filled, never after. -
scripts/shoddy-display.ps1/.shruns the three suites the gate is
not allowed to run -seedscribbler,seedturtle,seedplotter. They open
a real window, and GLFW needs a platform to create one on, which a hosted
runner does not have. The script reads the sameTST_EXCLUSIONSlist the
gate excludes them by, so a fourth windowed suite cannot be added there and
forgotten here, and it refuses on a machine with no display rather than
failing somewhere inside GLFW. -
scripts/shoddy-commit.ps1/.shstages and commits. It refuses on
main, which is the point of it rather than a courtesy, and-Forceis the
exception that has to be typed out. -
scripts/verify-twins.js, now a preflight step, proves every script in
the repository has its twin and that both offer the same verbs. The pair
had drifted before --yagainst-Yes- while the documentation went on
calling them interchangeable. -
tutorials/spiro/build.ps1, the twin that was missing. The tutorial is
meant to be copied out of the tree, so it finds the mill by itself rather
than borrowing the repository's shared helper.
Fixed
-
Non-ASCII text sent to
fettlewas corrupted on Windows.Console.In
decodes stdin with the console's input code page, 437 or 850 on a default
install, so the three UTF-8 bytes of an em dash were read as three separate
characters and written back out as UTF-8 in their own right: six bytes where
there should be three. The round trip was self-consistent, so the hash
matched and nothing downstream could tell. It reached further than writing:
a search for a non-ASCII pattern matched nothing and said so calmly. -
A new
.shwas committed without its executable bit.verify-permissions
walks tracked files, so the window between writing a script and adding it was
invisible to it - which is how v1.8.0 shipped four scripts that way and one of
them took the Release workflow down withPermission denied.shoddy-commit
now stages an unseen shell script withgit add --chmod=+x. -
shoddy-release.ps1tested the wrong thing for "am I at the repo root".
It compared paths that Git for Windows spells differently; it now asks git
directly. -
shoddy-release.ps1wroteDirectory.Build.propsthrough Windows
PowerShell's-Encoding utf8, which emits a BOM. The version bump now
writes UTF-8 without one. -
The shell twins accepted only
-ywhere the PowerShell scripts document
-Yes, and only as the first argument. Both twins now take either
spelling, anywhere in the line. -
sed -iinshoddy-release.shmeant something different on BSD sed
from GNU sed. The version bump no longer uses it.
Changed
- The gate runs its PowerShell steps under PowerShell 7. Windows
PowerShell 5.1 turns anything a native program writes to stderr into a
terminating error the moment a caller captures both streams, and writes a
BOM where it is told not to.pwshis probed once and used everywhere, with
a fall back topowershellwhere it is absent.
Documentation
-
WORKFLOW.mdandRELEASING.mdpointed atshoddy-release.ps1as the way
to release. It is the primitive underneath, not the front door: it builds,
tests, tags and pushes, while the doc gates, three of the four test lanes and
the green-receipt check are all skipped. Both pages now name
scripts/shoddy.ps1 release X.Y.Z, with a table of exactly what the wrapper
adds. -
The display-suite gap is written down in
WORKFLOW.mdbeside the command
that covers it, rather than being an exclusion list nobody reads.
v2.3.2
A Fettler release: fettle serve now applies edits to its declaration
without a restart ΓÇö a permission you revoke is gone on the very next
request.
Fixed
-
Editing
.fettler.jsonor.fettler.local.jsondid nothing to a
running server.serveread the declaration once at launch and held
that reading for the life of the process, so a task declared
mid-session did not appear until the client was restarted ΓÇö and, the
direction that matters more, a permission removed mid-session stayed
granted. The CLI never had this fault: every command is a fresh
process and reads the file anew. The server now does what the CLI
does ΓÇö both files are re-read before every request ΓÇö so an edit
binds on the next call: a granted task appears, a revocation takes.The re-read is unconditional rather than gated on a modification
time, so the behaviour is deterministic. A malformed edit refuses
every request, naming the fault, until it is fixed ΓÇö never falling
back to the last good reading, since a boundary held wider than the
file states is the one direction the tool must never fail in. A
declaration that goes missing refuses too, rather than re-discovering
one: the file found at launch is the file, for the life of the
server.--rootdeclares a boundary with no file behind it, and that
one alone stays as launched.
Documentation
- The Fettler README's registration section now states the re-read
contract in place of "a boundary fixed at that moment", and the
server's MCP instructions tell the model an edit to the two files
binds on the next request.
v2.3.1
A Fettler release: the guardrail closes a route it had been leaving open, the
two verbs that configure a machine can now run on a machine that has not been
configured yet, and the documentation gains a quick start.
Changed
-
fettle setupnow denies the shell as well as the built-in file tools.
The list it writes went from six entries to eight:BashandPowerShell
joinRead,Write,Edit,NotebookEdit,GrepandGlob.A shell is a complete route round the boundary, and not a second-order one ΓÇö
node x.jswrites anything anywhere and is not a writing command at all,
so no blocklist of commands could ever close it. Denying the two tool names
is as short, closed and platform-identical as denying the other six; it is
the commands behind them that never close, and those are yours to name.This stops your build and test commands the first time you run it. That
is the deny working, and the point at which you decide what to let through.
Allow back only what the project needs ΓÇö"allow": ["Bash(git status:*)"]ΓÇö
or better, declare them as tasks in.fettler.json, which run inside the
boundary. Never allow a command that writes files.
The exclusion list
has the reasoning and both recipes. -
doctorcheck 2.8 covers all eight names. A project with the six file
tools denied and the shell wide open used to report a clean bill of health ΓÇö
not because that was judged safe, but because nothing looked. It is now a
finding, anddoctorexits 1 rather than 0. -
doctorcheck B.17 exempts a narrowed shell allow.allow: Bash(git status:*)besidedeny: Bashis exactly the shapesetupnow asks for, and
B.17 matched on the tool name ΓÇö so it reported the recommended configuration
as a conflict and named "remove the allow" as the fix, which is the one edit
that breaks it. A narrowed allow onReadis still flagged: reading is
precisely what Fettler replaces.
Fixed
-
fettle setupandfettle doctorcould not run before.fettler.json
existed ΓÇö andsetup --localis what creates it. Every verb resolved the
boundary before dispatch, so the file needed to runsetupwas the file
setupwrites, and a first run in a new project exited 2. Both verbs now
fall back to the current directory when nothing at all is declared. The
fallback is read-only, reaches only a compiled-in list of well-known
configuration paths, and does not apply to any other verb; a configuration
that is present and malformed is still refused rather than fallen back from. -
fettle setup --hooksinstalled nothing when the deny list was already
complete. The hook sat after an early return taken whenever there was
nothing to deny ΓÇö so asking for it on an already-configured project, or at
--globalwithout--deny, reported success and wrote no hook. The deny
list and the hook are now decided independently and saved once. -
fettle setup --globalleft you with no working project. A global run
declares no tree and therefore writes no.fettler.json, and said so
nowhere. It now names the per-project command that creates one.
Added
- Quick Start ΓÇö
install it, register it for the machine, turn it on in a project, check it.
Four steps, one command each, with the real output of each and a table
routing every uncleandoctorverdict and finding to the page that clears
it. It is the second page in the Fettler sequence; the install and project
pages are the reference behind it.
Documentation
-
fettler-boundary.htmlis now
fettler-install.html.
The v2.3.0 release notes link to the old name and that link no longer
resolves; what it pointed at is now
Fettler §7. -
The three Fettler pages were reordered so each answers one question and the
steps appear in the order they depend on each other. Page one is what Fettler
is and asks you to type nothing; the install page is the machine, with the
MCP front end gathered into one section at the end rather than opening the
page; the project page is.fettler.jsonfirst, because every other step
needs it. -
Thirteen machine pages carried breadcrumbs pointing at catalog anchors that
no longer existed, and eleven of them displayed a category name that does
not exist at all. Both halves fixed, and the checker that missed them now
compares the label as well as the link. -
scripts/verify-docs.jsdropped any navigation item carrying a class of its
own, so an item could vanish from every page at once with the consistency
check still passing. -
Every code block on the Fettler pages has the site's copy button, and the
Fettler link moved to the top navigation row, which had room for it.
v2.3.0
Added
- Fettler ΓÇö file,
search and edit tools for a model and a script, with no shell in between.
The project is Fettler; the command isfettle. 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.vsixand 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.jsona 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.
executeis 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.jsonare refused by every write path at every
permission level, with their own outcomegovernedand 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.jsonand
.vscode/mcp.jsonrefuse 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 baresettings.jsonand
.vscode/settings.jsonstay ordinary;CLAUDE.mdis left out because it
persuades rather than permits.setupstill 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-credentialis 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 doctoranswers, 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-approvedsed -i, an allow on a built-in editor, an
instructions file that has never heard of Fettler. It writes nothing.
fettle setupwrites 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 NAMEreads one entry out without unpacking anything.--tail N
is the end of a log without needing its length.extractunpacks 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'sserverInfoandfettle --versionnow 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 gatenow 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 --resumeused 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.publishwas 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 samemills/<name>/binDLL, 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:MillGateserializes 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. SetShoddyBinScopeon 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.manifesthad lostjulian. 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 whenhosts/maui/**changes. A file inmills/was
breaking a lane inhosts/, connected by nothing that would ever fire.
verify-manifests.jsnow checks every manifest that exists on every gate
run.
v2.2.0
Added
machines/sinq.shoddy
— Shoddy INtegrated Query: the half of a query the rest of the library could
not say, which is the word by. Ordering by a key and by several keys
(OrderBy,OrderByDesc,OrderWith,ByKey,ThenBy,Descending),
grouping (GroupBy), joining (JoinOn,GroupJoinOn), the set words
(Distinct,DistinctBy,Union,Intersect,Except), walking a record
type that holds itself (Descendants,Offspring,SelectMany), the
extremes by a key (MinBy,MaxBy, and theirOpttwins), and the total
twins that answer anOptioninstead of aborting (FirstOpt,LastOpt,
SingleOpt,FindOpt). It re-declares nothing:Map,Filter,Foldand
Sortare still builtins andSumis still stats'.- Sorting by a key at last.
Sortis a builtin, ascending-only, and takes
no selector — handed records it stops the program with
SORT expects all NUMBERs or all STRINGs.OrderByDesc(staff, Pay)is now
an ordinary line. The sort is a stable merge sort, so equal keys keep source
order andThenBymeans what it says. - No adapter for arrays, vectors or matrices, because none is needed. A
Matrix'sCellsis already an array and an array is already a sequence, so
a matrix is queried through the same words as a list. seed-sinqputs thirteen of them at the reckoner keyboard in halifax and
sparky alike —SORTBY,SORTBYDESC,GROUPBY,MINBY,MAXBY,
FINDFIRST,TAKEWHILE,SKIPWHILE,DISTINCT,UNION,INTERSECT,
EXCEPTandCHUNK. The eight that take a selector take a quoted program:
{ 1 2 3 4 } [ 2 MOD ] SORTBY.- A worked example that the gate runs.
tst/sinq-demo.shoddyqueries a
customer/order graph, a catalogue, a self-holding org tree and a matrix in
one program, and is reproduced line for line on the machine's page — so the
page cannot show code that no longer compiles.
Changed
- Five machines dropped code they should never have had to write.
alg's
hand-rolled insertion sort is gone and its canonicalisation is n log n;
jsonandxmlboth carried a written apology thatSortwould not sort a
list of pairs, and both are gone with the per-key re-lookups they forced;
reckoner'sWORDSis aGroupBywhere it was three Defs;bool's
Quine–McCluskey dedupes withDistinctand scores each implicant once
instead of twice. seq's documented cost model was wrong, and is corrected everywhere. A
Shoddy list is contiguous underneath, soResthands back a copy of the
tail and aFirst/Restwalk is quadratic — 74 ms at 12,500 elements and
3,585 ms at 50,000. Lists are not cons cells. The machine header, the
seq page and both
AI grounding pages said otherwise and now say this.- Only self-tail-recursion becomes a loop, which is now written down
where it can be found. Mutual recursion costs a frame per element like any
other call, and the ceiling is about 20,000 frames — a hard process kill with
no line number.sinq's own suite runs fifty thousand elements for exactly
this reason; it is the only suite in the tree big enough to catch either
fault.
v2.1.0
Breaking
finno longer declares its own calendar. TheFinDaterecord and its
six date words are gone; the day-count and bond words now take aJulDate
from the new julian
machine. Build one withJulOf(y, m, d)and addInclude "julian.shoddy"—
the arithmetic is unchanged, so prices and yields come out identical.- At the reckoner prompt, the
FINDATEfamily is now spelledJUL*—
JULDATE,JULDATEOFand friends, from the new seed. Same date-dict shape,
so the day-count and bond words take the result exactly as before; only the
spelling of the making words changed, and a session that tries the old name
is told the new one.
Added
machines/julian.shoddy
— the calendar as arithmetic, and the one place the Gregorian leap rules are
spelled: checked and total date builders, day arithmetic, weekdays, and the
two day-number epochs (the 1970 serial and the astronomical Julian day) one
constant apart.clockgainsClockDate(), the single bridge from "now"
into that arithmetic.machines/ephemeris.shoddy
— the sky as arithmetic: sidereal time, altitude and azimuth for any
observer, the sun and the moon as sky positions, the moon's phase, age,
distance, rise and set, and the five naked-eye planets from Keplerian
elements. Pure throughout; every word takes a Julian day number.seed-julianandseed-ephemerisput both machines at the reckoner
keyboard —JULDATEandJULDAYNUMBERthroughEPHMOONRISE— in halifax
and sparky alike.- Numeric ODE solving in
eng: a fourth-order Runge–Kutta stepper over
quotations, scalar (EngOdeSolve) and systems (EngOdeSysAt), for the
motion problems the physics atlas works through. docs/charter.html—
the charter of the mathematical family: which machine owns what, the layer
map, and the rules that keep alg, eng, fin, geo, julian, ephemeris, lin,
math, matrix, neural, random and sparse one family rather than a pile.docs/physics.html—
the physics atlas: where each physics topic lives in the tree, with a worked
damped-driven oscillator.- Every machine page now opens with a history — "A Brief History of…" as
part of the intro, from the haversine to the ready reckoner, on all
forty-three pages instead of seventeen.
Changed
geo'sGeoJulianis now defined over julian'sJulDayNumber— one set of
leap rules in the whole tree, bit-for-bit the same answers.- The reckoner seeds are listed once, on the
reckoner page,
instead of being repeated in every machine's "Who Uses It" — and that
listing is now ground-truth-checked, both directions. geojoined the quick reference's Core numerics table, which it had always
been missing from.
v2.0.3
Typing "1/0" ALGSIMPLIFY at a halifax or sparky prompt used to end the
session — not the line, the session, taking the stack, the dictionary, the
registers and the tape with it, including anything defined but not yet
SAVEd. It refuses now, and the next line still answers. Nothing else in the
language changed.
Alongside it, the CI reds that a release run never sees, and the sparky
download finally pointed at from the places people actually start.
Fixed
-
A division by zero inside a typed expression aborted the mill.
AlgRead
was documented as total and was total for syntax only: the parser's/
branch called the abortingAlgDivOf, so any expression whose divisor came
out zero killed the process. It now refuses through a guarded twin,
TryAlgDivOf, and all five seedalg words —ALGSIMPLIFY,ALGEXPAND,
ALGFACTOR,ALGSOLVE,ALGDESOLVE— inherit the refusal without a line
of their own changing:> "1/0" ALGSIMPLIFY ?: ALGSIMPLIFY: DIVISION BY ZERO AT TOKEN 3The literal
/0was the least of it. The builders fold the divisor while
the parse is still running, so"1/(1-1)","x/(y-y)"and"sin(1/0)"all
reached the same abort with no/0anywhere in their text — which is why the
guard had to go in the parser and not into a scan of the string. -
0^-1quietly answered0. A zero base to a negative exponent is a
division by zero written another way, and the machine had two answers for one
question: abort for1/0, a wrong0for0^-1. Both spellings now refuse
in the same words, throughTryAlgPowOf. A positive exponent still folds to
zero as it always did, and a symbolic exponent whose sign is unknown is
now left standing as0 ^ xrather than guessed at. -
The fuzz lists had no algebra in them.
mills/halifax/test.shoddyand
mills/sparky/test.shoddybetween them carried a good spread of hostile
regex shapes and not oneALG*line, which is how a whole seed's text
boundary reached a release without ever being fed a bad string. Both lists
now include the zero-denominator shapes, and sparky's note recording the
omission as a known defect is gone with the defect. -
A weave could be read while it was being rewritten. The mill gate
serializes writers, but another project's compiler can hold the previous
weave open as a metadata reference while the winner rewrites it —CS0009
on the Linux runners, where both projects of a solution build abreast. The
weave now lands beside its target and renames into place, so a reader sees
the old file complete or the new one complete and never a truncated one. -
The host-blindness check refused the compiler's own comment.
MillGate
named a host lane insrc/, which is exactly what the check exists to
forbid; the comment now describes the shape without naming it. -
The MCP lane asserted a path MSBuild does not write. Its closure checks
namedartifacts/bin/sparky/release, but the project isSparky.csprojand
the artifacts layout capitalises the directory. A Linux runner does not
forgive that the way a Windows workstation does. The step had never executed
before — every earlier run died upstream of it — so its first run was its
first test. The publish paths were already explicit, which is why the
release shipped while this stayed red. -
The sitemap had silently lost eleven pages — sparky, halifax and tally
among the mills, and eight machines, several missing for months.
verify-docsnow rebuilds the whole expectation from the tree in both
directions, so a page without an entry and an entry without a page each name
themselves. Being ungated is how it rotted.
Changed
- The sparky download is offered where people start. The release page
carried it and nothing pointed at it. The README's install section, the docs
home and the setup page now each offer it beside the.vsix, and the
release workflow records asparkydeployment per release so the repository
home's Deployments sidebar lists it next to the Pages site.
Added
TryAlgDivOf(a, b)andTryAlgPowOf(b, e)in
alg.shoddy —
Result-answering twins of the two builders that can refuse. The aborting
forms are defined over them and keep their messages exactly, so existing
callers are untouched and there is one set of rules rather than a builder and
a validator that can drift apart.