Skip to content

Releases: Formicaria/Anthill

ANTHILL v0.3.8.75

Choose a tag to compare

@github-actions github-actions released this 16 Aug 08:52
71855ff

v0.3.8.75 - a documentation patch is verified as documentation

Qualification scenario 3 closes. It was the last of the twenty.

The defect: an escape hatch that was built and never reachable

docs_patch has required {diff, security_policy} — deliberately no build — since the policy table
was written. Nothing ever selected it. The planner emits patch_proposal for every patch, docs
and code alike; the alias maps that to code_patch; code_patch requires build. So a README-only
change has always been compiled with dotnet build -c Release, on the Director thread, before it
could be called verified.

v3.8.21's note in that same table worries at length about exactly this cost — "up to half an hour of
wall clock per code-patch task, serially, on the Director thread" — and removed test from the
default to contain it. It did not notice that the docs_patch row three lines below would have
contained it further, for free.

The task type cannot tell them apart: coder.docs_coder and coder.ui_coder both emit
patch_proposal. The patch's own paths can, and they are the honest source — what a change touches
is a fact about the change rather than a claim about it.

Conservative in the direction that matters. Every proposal must be a documentation path: one
.cs file among ten .md files makes the whole set a code patch, because a set applies as a unit
and is exactly as dangerous as its most dangerous member. An empty set is not documentation. An
explicit policy key is never softened by paths. diff and security_policy still run, the soldier
is still policy-inserted, and a docs patch that trips either is blocked exactly as before — this
narrows which deterministic build runs, and nothing about whether a reproducible no is final.

ScribeAnt's documentation-only restriction and this policy now read one predicate. Two copies of
"what counts as docs" would be two answers to a question the security boundary asks, and they would
drift toward the more permissive one.

Scenario 3, and the four defects between proposing and doing

Every one was found by trying to reach an outcome nothing had needed before: the tester's check
running in the original tree (v0.3.8.70), the tester having no operator seam (v0.3.8.73), a green
mission graded as an escalation (v0.3.8.74), and this release's. AppliedDocsPatchLifecycleTests
walks all nine gates between a proposal and a byte, asserts the file is absent beforehand, that the
operator's check verified it rather than dotnet_build, that no build ran at all, that the
evidence identifies its revision, that no break-glass event was recorded, and that the proposal left
proposed.

An earlier draft moved its check to a root-level file on the theory that a subdirectory target was
what broke it. That theory was never tested — the failure it was invented to explain was the
adaptive-stop defect — so the workaround is undone and the straightforward thing is used. A
workaround for a defect that does not exist is worse than none.

Item 1 — reconcile the documentation, given a form that lasts

This item keeps being absorbed into other releases and keeps coming back, because a reconciliation is
true on the day it is done and decays silently after. This release alone corrected three documents
that had sent work in the wrong direction, and HANDOFF.md — the file whose whole purpose is to be
pasted into a fresh session — opened with "The 3.8 line is CLOSED at v0.3.8.34" while the shipping
release was v0.3.8.74. That is not staleness; a handoff is read by someone who knows nothing else
yet, so a wrong one actively misdirects.

HANDOFF.md is now a pointer, not a snapshot: a table of where each answer always lives, the
working rules that are not obvious from the code, and the recurring defect classes. A snapshot has to
be rewritten every release to stay true and will therefore be false most of the time. Pointers stay
true on their own.

DocumentCurrencyTests makes the detectable half executable. Every file in docs/ is classified
CURRENT, HISTORICAL or POINTER — so a new document forces the decision rather than defaulting to
"current and quietly rotting" — historical ones must say so before their content starts, and no
current document may present a superseded release as the state of things.

It says plainly what it cannot do: it cannot tell whether a current document is correct. The
docs_patch_set chain that sent scenario 3 the wrong way named no version at all. This closes the
subclass a machine can see; the rest is reading.

Its own first run caught the trap this repository keeps finding in its guards — as of matched
"Provenance already carries most of this per artifact as of v0.3.8.57", a historical reference
inside a current document and exactly the construction that must stay legal. The guard was wrong, not
the document, so the pattern narrowed rather than the sentence changing.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.75
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.74

Choose a tag to compare

@github-actions github-actions released this 16 Aug 08:21
f79c8ad

v0.3.8.74 - a green mission graded as an escalation

ExecutionService returned one stop reason for two opposite situations, and the evaluator graded
both as a failure.

adaptive_stop came back from three call sites for either "the repair bound is spent and the
critical failure persists" — a real escalation — or "the controller wanted to add a verification step
and found the mission already has one", which is success. MissionEvaluation.Resolve mapped every
adaptive_stop to escalated before looking at a single task, verdict or piece of evidence.

That is not cosmetic. Auto-apply consumes the canonical evaluation and refuses anything that is not
completed_verified, so a mission whose plan included a verifier — the ordinary shape — could pass
every check, pass its security review, record deterministic evidence bound to its revision, and be
structurally incapable of applying its own patch. In production, not only in tests.

MissionStopReasons names the closed set; adaptive_stop_satisfied falls through to be graded on
the mission's own record, because a controller that looked, found nothing to do and said so must not
change the grade. AdaptiveStopMeaningTests proves both directions, so the fix cannot be "stop
escalating" — a spent bound is exactly when a person is needed.

The compile error that came out of naming the type was itself useful: ExecutionService already had
a private MissionStopReason(context, token) method that ASKS whether to stop. Both now read the
same vocabulary, so mission_timeout and mission_cancelled have one definition instead of being
literals in the producer and literals again in the evaluator that grades them.

Why scenario 3 is still open, and this release does not close it

It was written, it ran, and it stopped one gate short — so it is not shipped and the ledger says
OPEN. What it bought is a blocker named exactly, after two releases of naming it wrongly.

Auto-apply needs completed_verified, and reaching one took three findings. The tester's check ran
in the wrong tree (fixed v0.3.8.70). The tester had no operator seam, so a fixture workspace could
not produce a passing check (fixed v0.3.8.73). And now: the patch-set verification pipeline never
got that seam.
Verification.cs hard-codes check_id="dotnet_build" and contains no reference to
CheckSource, so every materialized patch is built with .NET whatever the workspace is. In a fixture
that build fails, the failure becomes a DeterministicBlock, and v3.8.22's rule — a reproducible no
is final — correctly makes completed_verified unreachable.

That rule is right and must not be weakened to close a scenario. The fix is to give the
verification pipeline the operator seam the tester already has, which is a change to a
safety-critical path and belongs in its own release with its own tests — not folded into one that
already carries an unrelated fix. Shipping a half-understood change to the verification pipeline
would be the partial thing here; shipping a proven fix without it is not.

It was diagnosed from the evaluation record rather than by inference, and only after two rounds of
guessing which layer said no. completed_verified is a conjunction of four independent layers and
the outcome code names none of them, so the assertion now prints all four plus the evaluator's own
explanation and every evidence row. One run then said it: structural complete, verification passed,
deliverable not_checked, no stop reason, and a deterministic build:fail stamped with the mission's
own revision.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.74
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.73

Choose a tag to compare

@github-actions github-actions released this 16 Aug 07:48
6fc1cc6

v0.3.8.73 - the report nobody wrote, and the operator half of a sentence from v3.5.0

The first live qualification run

It reported eight defects. There were three, and separating them is most of the value.

The operator report had no compiler. Commands, exit codes, durations, test totals, a role census
and medic activity were all model prose — BuilderAnt writes the operator answer by prompting a
model, and nothing in the colony assembled a report from records. There was no reporting code to have
a bug in. Five of the eight reported defects were that single fact seen through different columns.

The tell was Dispatched: a column that appears nowhere in this repository, holding statuses
(In Progress) the persisted vocabulary has no word for — the real one is the lowercase TaskStatus
enum. The column was invented and so was everything in it.

MissionReport compiles the record from persisted rows at finalization and stores it as the
operator_summary artifact with ModelInvolved = false. Checks come from the tester's own evidence
rows, the only place an exit code exists. The role census comes from AntRegistry, the only thing
that knows what exists. Times are computed from stamps, and a duration nobody measured renders as
"not recorded" rather than as a number. Compile(SqliteMemory, string) takes no text parameter —
the signature is the guarantee that no later edit can quietly let prose contribute.

The builder's narrative survives, demoted: it is now forbidden to state a command, exit code,
duration, timestamp, test count, file count or role census. A prompt cannot stop a model inventing
figures, but it can stop asking for them, and with the compiled record beside it an invented figure
has no reason to exist and nowhere to land. This is the division ScribeAnt has had since v3.8.28 —
release notes assembled from the mission's own results, never from a model answer. That role was
already right; the operator report was the one that was not.

The web ant ignored a named source, and was never looking for one — the query was
goal + description, so a domain the operator named was just more words for a search engine to
weigh. One recognised site now becomes a site: filter. Two mean a comparison as often as a target,
so nothing is guessed: the failure mode this release is about, in miniature.

Two reported defects were not defects, and that is recorded so nobody fixes them.
"Finalized while tasks were In Progress"Queen.FinalizeMission carries a v2.26.0 invariant
forcing any non-terminal task to failed with internal_runtime_defect and failing the mission
closed. "The verifier fails open" — the Queen always hands it the evidence store, and an empty
evidence list resolves to Unknown with "nothing has been verified". The PASS the operator read was
the builder's prose. Both are pinned by tests now, because "we checked and the mechanism was already
right" is a finding this repository keeps having to re-derive.

The report was one line from something true, and that line is closed anyway: with no evidence store
at all the verdict used to fall through to Parse(text). Unreachable in production, and exactly the
shape S3 closed on the neighbouring arm.

The first attempt at closing it was wrong, and the way it was wrong is the release in miniature.
It refused any verdict parsed from the verifier's text — and broke two tests that were right.
text is not always model prose: with useOllama false there is no model in that ant at all, and
the text is the static verifier's own deterministic evaluation of task states. S3 preserved that path
deliberately, calling its removal "rigour's costume on a regression"; the refusal would have made
every offline mission unverifiable in order to close a hole production cannot reach. The
discriminator is not "did this come from text" but "did a MODEL write it". A model's confidence now
cannot promote a verdict; its doubt is still heard, because doubt costs nothing and confidence is
what has no standing.

operator_summary's schema entry said "named by ADR-004; produced by nothing yet". It has a producer
now, so the entry says so — a stale shape declaration in the table that describes what everything
else writes would be this repository's own recurring defect, one level up.

The operator half of a sentence written in v3.5.0

WorkspaceCapabilityManifest has carried the same exit gate since v3.5.0: "verification commands
come from the manifest or operator configuration, never model invention."
The manifest half was
built that release. The operator half never existed.

v0.3.8.71 established what that cost. A workspace the adapters do not recognise has no usable checks
at all: CheckCatalog.Register is documented as the "operator/test extension point" and is reachable
only by naming a check id in task text that ExecutionService writes — not the operator. The
fallback needs a project; adding one makes it worse, because a detected workspace runs every adapter
check by design. Qualification scenarios 3 and 15's last edge had both sat behind that.

workspace_checks is the missing half. An operator declares id, command, arguments, timeout and
enabled state in ANTHILL's own configuration. Non-empty replaces detection for the installation —
an operator who states what verifies their workspace is stating a fact about it, and appending the
detected checks back on would make the setting advisory. It is announced at startup like the roster
is, because a replacement nobody can see is a replacement nobody can audit.

It is not a file in the workspace, and that is the load-bearing part. There is deliberately no
.anthill-checks.json. WorkspaceAdapter's own doc says keeping detection and execution apart is
"what stops an agent that can edit a repository from editing the thing that checks it" — a check file
inside the tree would have handed every coding agent the power to rewrite its own exam. The
convenient design was the unsafe one. PolicyScan.allowlist_tampering learned the key the same day
it was created, so a patch proposing to edit it is a blocking finding like every other allowlist
edit; and a built-in id cannot be redefined, because dotnet_build means one thing across the
auto-apply verify path, the graduation record and every changelog entry that names it.

CheckSource is one decision function, because there were already two. The tester selected with
manifest.IsEmpty ? CheckCatalog.Ids : manifest.Checks; the runner resolved with
manifest.Find(id) ?? CheckCatalog.Get(id). Two spellings of one rule — and the runner's own comment
names the failure they invite: "Two components disagreeing about which catalog is authoritative is
how a tester selects an id the runner then refuses."
Adding a third source to both by hand would
have been a third chance to disagree. NeitherCallSite_SpellsThePrecedenceItself refuses the old
spellings by name.

Refusals are reported at LOAD rather than at dispatch: a missing command, an id with whitespace, a
built-in collision, a duplicate, an out-of-range timeout. One bad entry costs its own place and
nothing else — throwing would turn a typo into an unverified installation, and dropping it silently
would let the tester report PASS over a check set nobody chose.

Qualification scenario 15 closes

EarnedRepairLifecycleTests.ACheckFailsBecauseOfTheProposal_AndPassesBecauseOfTheRepair.

v0.3.8.69 gave scenario 15 a goal that earned eleven roles honestly and recorded the one that stayed
decorative: the tester's failure was environmental. A materialized revision in a temp directory
has no build, so dotnet_build failed for a reason the patch had nothing to do with, and the medic
then repaired a failure the change had not caused. The trigger was real; the failure's relationship
to the change was not.

Now an operator-declared check passes only when VERIFIED.md exists in the tree it runs in. The
coder's first proposal omits it and the check FAILS against revision one. The medic hands back. The
second proposal adds it and the same check PASSES against revision two. Nothing environmental changed
between the runs — only the patch did.

It needs two earlier releases to be true, which is why it is the right consumer for them: v0.3.8.70,
without which the check ran against the original tree and no patch could change an outcome, and this
one, without which the check could not be declared. And it asserts the operator's check ran rather
than dotnet_build, because "the seam is wired" is exactly the claim that passes while a fallback
quietly runs instead.

Scenario 3 is now the last open one, and its remaining work is only the apply step — a script
book rather than a blocker. ANT_EXECUTION.md gains the precedence table; the ledger header's claim
that scenarios 3, 4, 7 and 15 "still need" a composed Queen-driven run is corrected rather than
deleted, because it was true when written and stopped being so without anything failing — the exact
rot the ledger exists to prevent, in the ledger's own header.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.73
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.72

Choose a tag to compare

@github-actions github-actions released this 16 Aug 03:46

v0.3.8.72 - the fix that would have looked right

The sweep for other copies of v0.3.8.71's defect — a scanner reading a serialization instead of the
values — found the more useful thing one layer up: the obvious fix was wrong, and it would have
shipped looking correct.

What was nearly shipped. v0.3.8.71 fixed the soldier at the feed (DecodeForScanning). The
follow-up's first move was to also widen PolicyScan.secret_material to tolerate an escaped quote
(\"), on the reasoning that the feed is not the only way encoded text reaches a scanner. That
allowance does nothing. Json.Dumps leaves JsonSerializerOptions.Encoder at
JavaScriptEncoder.Default, which never emits \" — it emits a " unicode escape, and treats
<, >, &, ' and + the same way. The widened pattern would have been exactly as blind as the
original while reading as fixed, and a guard written by hand-typing "the escaped form" would have
agreed with it, because both would have been guessing at the same wrong encoding. The same widening
was drafted for ArchivistAnt.SecretLike and has been reverted for the same reason.

So the rule is unchanged and the layering is the fix. A scanner that tries to recognise text
through an encoding has to know every encoding, and is wrong the first time one changes. Patterns
match source; callers hand them source. That is the rule this repository already applies to
containment (PathContainment) and to test collections — one place answers each question — applied
to policy scanning.

SecretPatternEncodingTests never hand-writes an encoding. Every encoded sample comes out of
Json.Dumps, the same call RecordPatchArtifact makes, and every decode goes through
DecodeForScanning. If .NET changes its default encoder these tests still describe the truth,
because they never claimed to know what the escaping looks like. The property is not "the rule
handles escapes" — it is "the rule is never asked to". NoScannerIsHandedARawArtifactPayload is the
layering rule enforced, and it says plainly what it cannot see: an adjacency check in the file where
the two meet, blind to a payload arriving through three helpers.

The rest of the sweep, including what was fine. PolicyScan.Scan has two callers, and the second
is why this hid for two releases: SecurityPolicyVerifier reads r.ChangedPath and r.NewContent,
raw strings that are never serialized — so one caller proved the rule healthy while the other could
not use it at all. ArchivistAnt.SecretLike has the same shape with the failure running the other
way (a miss writes a secret into durable memory rather than declining to block a patch), but its
inputs are plain strings and it never sees a payload; it is left alone, with the reasoning recorded
against it. TaskScheduler.SensitiveAssignment was already encoding-tolerant. Recorded because
"checked, and fine" is a finding.

Also: the duplicate encoding tests that shipped inside SoldierBlockLifecycleTests are deleted in
favour of the single file above — one of them described the escaping as \", which is the mistake
this release is about, sitting in a test that passed.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.72
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.71

Choose a tag to compare

@the-x1x1 the-x1x1 released this 16 Aug 03:30
337ccf0

v0.3.8.71 - the patch arrived escaped

The soldier could not find a quoted secret in a patch. Since v3.8.25.

This was found by a test written for something else, which is the only reason it was found at all.
The scenario 7 fixture proposes a deployment runbook that pastes in a working credential — the
ordinary way secrets reach a repository, not an attack — and asserts the soldier blocks it. Its first
run returned an empty warnings list. The block never happened.

secret_material is the most severe rule in PolicyScan: critical, blocking, and the one v3.8.26
widened after a capital K let a secret through. Its pattern needs a quote immediately after
[:=]\s*. RecordPatchArtifact stores proposals as JSON, so

api_key = "sk-live-9f3a2b7c4d1e"

reaches the soldier as

"new_content": "…api_key = \"sk-live-9f3a2b7c4d1e\"…"

and the character after = is a backslash. Every quote in every payload is escaped, so the rule
has been structurally unable to fire on a quoted secret in patch content for as long as the soldier
has had patch content to read. It could only ever match the task description — which is prose, which
is precisely the blind spot v3.8.25 existed to close.

That release's note said it plainly: "a policy engine that scans a description cannot find a secret
in the change."
Right about the problem; the fix delivered the change in a form the rule still could
not read. The patch arrived, escaped. And the failure is silent in the worst direction — the
review reports "0 blocking findings", not "I could not read the content", so a clean scan of
undecoded material is indistinguishable from a clean scan of a real one.

SoldierAnt.DecodeForScanning now hands PolicyScan the artifact's values, recursively, rather
than its serialization — every string, keys included, not the two field names this payload happens to
use. A decoder that read named fields would stop covering a field the day someone added one, which is
this defect's own shape a second time. A payload that will not parse is scanned raw rather than
dropped: a malformed patch artifact is when a review should be more suspicious, not less.

AQuotedSecret_IsFound_InTheDecodedPatch_AndNotInItsSerialization pins both halves against
PolicyScan directly, so the claim is about the rule and the encoding rather than about the mission
plumbing that surfaced it.

And qualification scenario 7's composed half closes

SoldierAntTests and DeterministicBlockTests have proved since v0.3.8.57 that the soldier reads
the real patch set and that its block cannot be argued away by model text. That is a claim about the
soldier — and, as above, "reads" turned out to be doing more work in that sentence than it could
bear. The scenario's other claim — that a
block stops a real lifecycle — is about everything downstream believing it, and was open.

SoldierBlockLifecycleTests drives it end to end. A Queen mission on the scripted provider proposes
a deployment runbook whose content pastes in a working credential — the ordinary way secrets reach a
repository, not an attack. The soldier is policy-inserted on the patch set's existence, no plan
names it, PolicyScan.secret_material fires as a blocking finding, the deterministic_block marker
reaches the persisted task result, the mission cannot reach a positive canonical evaluation, and
AutoApplyRunner.Run refuses to write.

The write gates are deliberately ON for that run, which is the only configuration in which the
assertion means anything: with autonomy_autoapply_enabled, patch_application_enabled and
file_writing_enabled off, nothing would be written whatever the soldier decided, and the test would
pass while proving nothing. The recorded refusal reason is asserted too, so the absence of the file
cannot stand in for a block that never happened.

Scenarios 3 and 15's last edge are blocked, and this release says on what

Both need one thing: a mission in a fixture workspace whose tester passes. The plan has assumed
for four releases that this was a missing script book. It is structural, and all three routes are
closed:

  • A registered check cannot be selected. CheckCatalog.Register is documented as the
    "operator/test extension point", and TesterAnt picks check ids by matching them against its
    task's title and description. For a policy-inserted review those are fixed strings built by
    ExecutionService from the patch set id. A mission cannot mention a check id, so the extension
    point is unreachable by the one role that exists to run checks.
  • The fallback needs a project. No manifest and no matched id means {dotnet_version, dotnet_build}, and dotnet build in a directory with no project fails.
  • Adding a project makes it worse. A .csproj fires the .NET adapter, and a detected workspace
    runs every check the adapter declares — build, test and format — deliberately, because "a
    tester that picked a subset would be choosing which failures the colony is allowed to notice." A
    minimal fixture passes the first and fails the other two.

None of that is a defect in isolation; each piece defends something real, and adapter detection is an
explicit exit gate. The gap is one clause: verification commands are supposed to come from the
manifest or operator configuration, and the second half has no path to the tester.

TheTesterHasNoSeam_ForAFixtureWorkspace pins all three facts against the source that establishes
them, so the next attempt starts from the finding instead of rediscovering it — and fails the moment
any of them stops being true, at which point it should be deleted and replaced by the scenarios it
is standing in for. The ledger and docs/PLAN.md now say the same, in place of the script-book note
that sent the work in the wrong direction.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.71
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.70

Choose a tag to compare

@the-x1x1 the-x1x1 released this 16 Aug 02:55
9ce8db8

v0.3.8.70 - the check that judged the wrong tree

Surveying qualification scenario 3 found a source defect in the evidence path, which takes priority
over the scenario by §1b's own argument — existing autonomy being trustworthy before the colony does
more.

RunAllowlistedCheckTool chose its working directory with manifest.IsEmpty ? _workdir : manifest.Root. That reads as "no workspace in scope, use the configured directory" and does not
mean it. The manifest is empty when the workspace adapters detect no project type at the scoped
root — a statement about what is in the directory, not about whether a directory is in scope.

So the sequence was: ExecutionService materializes the patched revision, enters a
MissionWorkspaceScope bound to it, dispatches the tester inside that scope, and stamps
task.RanRevisionId = revision.RevisionId — unconditionally. Meanwhile the check ran against
_workdir, which is AnthillRuntime.AllowedWorkspaceRoot: the original, unpatched tree. The record
said the tester judged the revision; the process had run somewhere else.
A declaration disagreeing
with the runtime, in the evidence path, on the side that reports success. This is pending item #44,
"bind Tester and Soldier to the exact patched revision".

It survived because on this repository it is invisible — ANTHILL is .NET, every materialized revision
carries .csproj files, the adapters detect them, and manifest.Root happens to equal the scoped
root. It bites on project types the adapters do not detect, and on docs-only patches, which is
exactly scenario 3's subject.

The fix separates the two questions the one flag was answering: the scope answers "where", since
that is what it was built for and the same value WorkspacePathGuard confines writes to; the
manifest keeps answering "which checks exist", unchanged.

CheckWorkingDirectoryTests does not assert on the code, deliberately — a test reading the branch
would have passed either way, and a test run against ANTHILL's own tree would have passed either way
too. It builds two directories differing only in which holds a marker file, scopes the mission to
one, and runs a declared check that succeeds only where the marker is. The exit code is the answer to
"where did you run". It is proved from both sides, so the fix cannot be "always succeed", and the
unscoped case is pinned so the CLI's and API's ordinary behaviour is unchanged.

And the catalog can now be put back the way it was found. CheckCatalog.Register called itself a
test extension point and offered no way back, so every check a test added stayed in a process-global
allowlist for the rest of the run — four test classes do it. That is the shape of the two static
leaks v0.3.8.69 closed, and it reaches further than it looks: TesterAnt selects from
CheckCatalog.Ids when the manifest is empty, matching ids against the task's own title, so which
checks a later mission can be asked to run depends on which tests ran first. Unregister refuses
built-ins, the same rule and reasoning ToolRegistry.Unregister carries.

A correction, and a note this release cannot make in the right place

v0.3.8.69 said the tester's failure in the composed lifecycle was environmental because "a
materialized revision in a temp directory has no build". The conclusion was right and the reason was
wrong: the check never entered the revision. That entry is shipped and therefore frozen, so the
correction lives here, which is what the frozen-changelog rule is for.

Qualification scenario 3's chain was also wrong, in the plan and in the ledger. Both described it
as passing through a typed docs_patch_set. There is no such pipeline and there should not be:
docs_patch_set is produced only by the scribe, its payload is {targets, source_mission, requires_approval: true}, its own artifact title says the scribe holds no apply permission, and
nothing in src/ consumes it. It is an approval request. Following the old note would have meant
writing an applier for an artifact deliberately designed never to be applied — the second time in
three releases that a ledger entry would have sent the work somewhere the code does not go.

What actually separates scenario 3 from scenario 4 is the word apply: every lifecycle test runs
with patch_application_enabled: false, so no test has driven a change onto disk through the Queen
and asserted the file is there. Applying needs a passing tester, and a passing tester needed this
release's fix. Both records now say that.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.70
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.69

Choose a tag to compare

@github-actions github-actions released this 16 Aug 02:38
c781434

v0.3.8.69 - a goal that earns its roles

Qualification scenario 15 asks for one mission reaching all twelve roles through their production
triggers, with no role invoked to satisfy a count. v0.3.8.68 established that the first clause
was already met and the second was not, and that the existing test failed it in the open: a goal of
"Add a short colony note to the documentation" with a ui_cartographer task titled "Map the frontend
surface", whose own answer was "no UI surface is touched by a documentation note." The web ant's
was the same shape. The remaining work was named there as a goal, not a bigger plan. This is
that goal.

AGoalThatEarnsTheRoles_LeavesNoRoleAnsweringThatItHadNothingToDo runs a mission that changes a
UI route and documents it. The workspace holds a real console page with two page-* regions, three
functions and two API call sites, so the cartographer's map is extracted from that file — change
the page and the assertions change with it.

The map is load-bearing, not merely present, and that is the strongest available form of "not
decorative". It was found by reading UiChangeGate rather than assumed: the coder's patch touches
index.html, and the gate refuses a UI change unless the mission holds a ui_map that is both
unmutated and schema-conformant. The coder's completion is therefore reachable only through the
cartographer's output. In the earlier mission the cartographer failed permanently and nothing
noticed, because nothing depended on it.

ScriptedWebSearchTool gives the web ant a real search, and applies the reasoning provider's own
argument one adapter over: substitute at the outermost boundary, leave everything behind it real.
The socket is faked; URL decoding, SSRF refusal, dedupe by normalised URL, domain quality scoring,
the confidence threshold, SaveSourceRecord and both pheromone trails are production code. It is
fail-safe by construction — scenarios shadow the module's tool by registering over it, and the tool
underneath stays gated OFF, so a shadowing failure produces a deterministic refusal rather than a
unit test that quietly makes network calls.

The clause is asserted, not described. "No role invoked to satisfy a count" needed an executable
meaning, and the earlier mission supplied one by failing: its decorative roles did not merely give
thin answers, they ended blocked (the web ant, on the search gate) and failed_permanent (the
cartographer, "no UI files could be read"). A role given nothing to work on cannot finish, and the
runtime says so in the status field. So the test asserts that no planner-selected role ends
blocked or permanently failed — scoped to the planned roles deliberately, because the tester's
failure here is real and expected, and folding an inserted role's honest failure into that clause
would make the assertion answer a different question.

Scenario 15 moves to PARTIAL, and the partial is precise. What remains is one thing: the tester's
failure is ENVIRONMENTAL — a materialized revision in a temp directory has no build — so the medic's
trigger is real while the failure's relationship to the change is not. Closing 15 needs an
allowlisted check that fails BECAUSE of the proposal and passes after the repair. That is the last
decorative edge, it is named in docs/PLAN.md, and it is not implied by these tests passing.

And a fix that shipped incomplete three releases ago

Adding the test above made ColonyAcceptanceTests.ScenarioA fail on "the default plan is research →
build → verify". Same defect as v0.3.8.60, same trigger — a new test class changing the order inside
a collection — and it survived that release's fix because the fix was only half of one.

v0.3.8.60 found ModelReliabilityTests flipping AnthillRuntime.UseOllama true while a mission was
running, and answered it by putting both classes in one collection. That was right and insufficient:
the mutation was never restored. Serialization removed the concurrency, so the flag stopped being
flipped mid-mission and started being left true for everything scheduled after that class instead.
The symptom moved rather than went away — ScenarioA now reached a live local Ollama, planned
dynamically, and failed on a two-task plan a model wrote. Membership in a collection is not custody
of a value; serialization only decides who inherits the leak.

ModelReliabilityTests now captures and restores, like every other class that touches the flag.
EveryTestThatMutatesAModelRoutingGlobal_AlsoCapturesThePriorValue is the assertion the guard
file was missing — its three existing checks are all about who runs beside whom, and every one of
them passed throughout. It states plainly what it cannot see: a capture is not a restore, and it
catches the case that actually happened rather than claiming more.

Its first run then caught something about itself, worth recording because it is this repository's
signature defect turned on a brand-new guard. The two halves were plain substrings —
"AnthillRuntime.UseOllama = " to find a mutation, "= AnthillRuntime.UseOllama;" to find the
capture. The first is a suffix match and sees a fully-qualified name; the second is anchored and does
not. So it reported ColonyAcceptanceTests — which captures and restores correctly — as a leak,
because that file spells the same statics Anthill.Core.Configuration.AnthillRuntime.…. The false
positive was the cheap half: the same asymmetry means a REAL leak written with a qualified name would
be flagged, and then silently forgiven the moment anyone added an unqualified capture elsewhere in
the file. A guard whose two halves disagree about how a name may be spelled is answering a question
about spelling, not about custody.
Both directions now come from one pattern per global.

And ScenarioA is pinned offline, because the leak only exposed the gap. It asserts the shape of
the DETERMINISTIC FALLBACK plan — the only planner that has a default — so an outcome that changes
when a model happens to be installed was never deterministic, and closing the leak alone would leave
it one config change from flaking again. Everything else in those scenarios stays real; only the
planner's source of a plan is pinned, and it is pinned to the one the file makes assertions about.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.69
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

ANTHILL v0.3.8.68

Choose a tag to compare

@the-x1x1 the-x1x1 released this 16 Aug 02:09
3b2c17d

v0.3.8.68 - the guard that was right for the wrong reason

Two corrections to the record, and one guard replaced with the one it should have been.

A test failed, and it was correct. NoTwoReleaseCommits_ClaimTheSameVersion reported that
0.3.8.60 is claimed by two release commits. It is: 3ec0366 (#16) is the real v0.3.8.60, and
9198dd5 (#23) is v0.3.8.67 committed under v0.3.8.60's subject line — the stale
RELEASE_MSG.txt that v0.3.8.67's own notes describe, reaching git commit -F. Nothing about the
shipped artifact is wrong: the tag v0.3.8.67 points at 9198dd5 and the tree in it is v0.3.8.67's.
Only every human-readable account of the release is wrong, which is why a green build could not
see it.

But that guard caught it by coincidence, and the coincidence is the finding. It fires on a
version claimed twice. A stale notes file only produces a duplicate if it happens to hold a
previous release's text — which it did. A file holding a draft, a placeholder, or a version that
never shipped would have passed it, and passed everything else too. The guard answered a question
adjacent to the one that mattered and answered it right, which is the most misleading way for a
check to be useful.

TheSubjectOfATaggedRelease_NamesThatRelease asserts the property that was actually violated:
if a tag's commit subject uses the v<version>: form, the version in it is the tag's own. It is
independent of what any stale text says. It fires on exactly one tag in the current history —
v0.3.8.67 — and that one is recorded in MisnamedReleaseCommits with its reason, because history is
not editable and rewriting it to make a guard green is the wrong direction.

The subject shape stays optional on purpose. v0.3.8.61, .65 and .66 are tagged on commits whose
subjects describe the work rather than the version; that is a legitimate style, and a guard that also
demanded the form would fail three honest releases. A check that is wrong about releases that were
fine is one people learn to override.

ReleaseNotesTests is the preventive half, shipped here alongside it: RELEASE_MSG.txt, when
present, must open with the runtime version and match the changelog's top entry. Absent is fine and
deliberately so — it is a release-time artifact, not tracked source, and requiring it would fail
every ordinary build. What must never happen is a present file describing a different release,
because that is the state that gets committed. The file is now derived from the changelog rather than
written twice; two copies of a release's story is one copy that eventually disagrees.

The scenario-15 ledger was under-crediting work that exists. QualificationMatrixTests marked
scenario 15 OPEN and cited TwelveRoleEndToEndTests, while
CodePatchLifecycleTests.AllTwelveRoles_RunThroughTheirRealTriggers_InOneComposedScriptedMission
already does most of what 15 asks: a real Queen mission on the scripted provider, eleven roles as
task rows, tester and soldier proved inserted rather than planned, archivist reached after
finalization. Pointing at the wrong file kept that invisible.

What scenario 15 actually still fails is its own clause "no role invoked to satisfy a count", and
the existing test fails it in the open: the goal is "Add a short colony note to the documentation",
the plan contains a ui_cartographer task called "Map the frontend surface", and the cartographer's
own scripted answer is "no UI surface is touched by a documentation note." The web ant's is "no
external sources are needed for an internal note."
Two decorative roles, planned so the count
reaches twelve and saying so when asked. The ledger note and docs/PLAN.md item 3 are corrected to
name that, and to say the remaining work is a goal, not a bigger plan — a mission that changes a
UI route, updates the doc describing it and trips a check would earn the cartographer, the web ant,
the tester, the medic and the scribe each on its own trigger.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.68
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash

v0.3.8.67 - the fence that made the prompt unparseable

Choose a tag to compare

@the-x1x1 the-x1x1 released this 16 Aug 01:53
9198dd5

v0.3.8.67 - the fence that made the prompt unparseable

A field report: a mission reached builder.result_compiler, the builder invoked Claude Code, and the
CLI answered error: unknown option '--- BEGIN UNTRUSTED MISSION GOAL --- …'. No ant executed. It
read as a colony defect and was a transport one — the prompt never reached a model.

Self-inflicted, in v0.3.8.60. That release put UntrustedBlock at the START of the coder,
builder and verifier prompts, so each began --- BEGIN UNTRUSTED MISSION GOAL ---, and that string
was the value of -p. An option parser will not take a value beginning with -: it read -p as
valueless and the fence as an unknown option. The device added to make untrusted input legible is
what made the prompt unparseable.

What the review got wrong, and why it changes the fix. The report said Anthill "builds one
command string" and should "use .ArgumentList, not a manually concatenated command string". It
already does — AgentCliDiscovery.BuildPsi adds discrete argv entries with UseShellExecute = false,
and AgentCliCatalog.BuildArgs names that as the security-relevant decision in the file. Quotes,
semicolons and backticks were never a problem here, and the escaping regression tests it proposed
would all have passed on the broken build. The failure was the CLI's own option grammar, not shell
quoting, so the fix is the CHANNEL rather than the escaping.

Two changes; either fixes this instance, together they close the class.

  • The prompt travels on stdin for agents that read it there. Claude Code's non-interactive mode
    does, which is documented, and nothing about a leading character can matter to a stream. Its
    argument lists now carry flags only — PromptArgs is ["-p"] with no {prompt} to substitute, so
    the text cannot arrive twice or arrive as an option by accident. PromptOnStdin is declared per
    agent and false where the behaviour is unverified, because assuming one CLI works like another is
    what put the prompt in argv to begin with.
  • UntrustedBlock fences with === instead of ---. = means nothing to an option parser. This is
    what protects the four agents whose transport is still argv.

Stdin is written before stdout is drained and the pipe is closed after: an agent that reads
its whole prompt first blocks until EOF, so a forgotten close turns a working transport into a hang
that the timeout then reports as the agent being slow — sending anyone debugging it to the wrong
place.

Tests. AgentCliTransportTests asserts a hyphen-leading prompt never becomes an argument, using
the exact string that broke; that the fence no longer opens with a hyphen while still marking both
ends; that both provider transports pass the prompt to stdin; and that the pipe is closed and written
before the drain. Awkward prompts — multiline, quoted, backticked, Unicode, Windows paths — are
covered too, recorded as already working rather than as the defect. Not proved here: no test
starts a real agent and round-trips a prompt through its stdin; that needs a binary the suite can
rely on across three platforms, and the transport is currently proved by the field report that
produced this fix.

Not fixed here, both from the same report and both real: the planner turned an observability
audit into "Synthesize condensed implementation plan", and ComposeMissionGoal appends the recent
transcript to every mission goal — which is how a conversation about prompt injection ended up
inside a later mission's context.

ANTHILL v0.3.8.66

Choose a tag to compare

@github-actions github-actions released this 15 Aug 22:25

v0.3.8.66 - the forward program resumes: evidence identity is mandatory for promotion

§2 item 2, and the last path closes. Auto-apply has refused a patch set whose evidence judged
a different revision since v0.3.8.57 — but the canonical evaluator never asked the store at all,
so correct test results about the WRONG TREE could still reach completed_verified outside the
auto-apply path, reinforce learning, and stand in the record as a verified mission.

The canonical evaluator now consumes the store's own testimony. A mission that materialized a
patch requires deterministic, passing evidence whose identity — revision id, patch-set hash, tree
hash, the Evidence.Judges() triple — names the FINAL revision. Earlier repair generations
cannot promote (patch set A's green run says nothing about patch set B), rows with no identity
cannot promote new work (legacy and unpatched-workspace evidence stay readable for history), a
model review naming the right tree still cannot promote (deterministic means deterministic), and
an unreadable store fails closed — §1b S3's direction applied at the last place a mission becomes
a verified success. The evaluator version bumps to evaluator-v3, so a persisted evaluation says
which rules graded it: the constant whose documented purpose had been exercised exactly once now
earns its keep.


Docker: docker pull ghcr.io/formicaria/anthill:0.3.8.66
LXC: curl -fsSL https://raw.githubusercontent.com/Formicaria/Anthill/main/deploy/lxc/setup.sh | bash