Released: 2026-09-07 21:09:22 (UTC+8 台北)
-
BREAKING: AGY 1.1.12 or newer is now required, and an older AGY is refused by
name. Runagy update. This replaces seven capability gates
(supportsAgyStdinPrompt1.1.2,supportsAgyStructuredOutput1.1.8,
supportsAgySlashCommandOptOut1.1.9,supportsAgyModelSelectionand
supportsAgyWorkspaceDir1.1.10,supportsAgyReadOnlySlashCommands1.1.11,
supportsAgyStreamJson1.1.12) and every fallback behind them with one check
indetectEngine.The gates were correct when written. What made them worth removing is that
they served users no one can see while being the only code the maintainer
could not run: six of the suite's eight skipped tests were the old-version
paths, skipped on Windows because the AGY stand-in reports Node's version and
cannot express a chosen one. Least-run code and least-tested code, the same
code. One gate was worse than untested — AGY 1.1.5 through 1.1.9 accept
--model/--effortand then ignore them in headless runs, so below it the
plugin quietly ran a model the user did not choose. A refusal that names the
version found and the command that fixes it is the honest version of that.1.1.12 is the highest floor that removes anything and the lowest that removes
everything: every gate sat at or below it, and no behaviour above it is
version-branched (AGY 1.1.20's exit-code change is absorbed byfailedExit
without asking the version).A version that cannot be parsed does not block the run.
agyMeetsFloor
answersok/too-old/unreadable, and onlytoo-oldrefuses; an
unreadable version runs and says it was not checked. The alternative turns one
cosmetic change toagy --versionupstream into an outage for every user at
once, and nothing here can tell that apart from an odd local build. The floor
is enforced against versions that are readable and too old, never against
silence.The floor is checked on both routes into AGY, not only the explicit one.
autoreaches AGY exactly when gemini has no usable credential, so an
unsupported AGY there is not a soft fallback — it is the engine, and it is
refused by name rather than run with--modelsilently dropped.
On that route the refusal does not offer--engine geminias the way out:
routing only reached AGY because gemini had no usable credential, so it names
both problems instead of sending the user to a second failure. A sub-floor AGY
merely sitting on PATH beside a working gemini is not reported at all.setupnow answersunreadableon both routes too. The two floor branches had
drifted apart:too-oldalready spoke about the AGY that would actually run,
whileunreadablespoke only under an explicit--engine agy. So underauto
with no gemini credential,/gemini:setupsaid nothing about a version it had
failed to read, and the first real command then said it — the same fact, two
answers, depending on which surface was asked. The narrowing that keeps a
stale AGY beside a working gemini unreported is unchanged, and now has a test
of its own rather than being a side effect of the narrower condition.The version is read from the start of
agy --version(bare1.1.25, or a
agy/antigravity/vprefix), not from the first pair of numbers anywhere in
the line. An unanchored match readantigravity (node 18.2.1)as AGY 18.2.1
and certified it; both misreadings are nowunreadable, which fails open.The docs that described the replaced gates are updated with them: the AGY
transport fallback (README Security,docs/known-diffs.md) contradicted the
stdin-only bullet five lines above it,--probe-agy's "1.1.11+" caveat
described a version the floor now refuses before the probe is reached, and
docs/MODEL_COMPARISON.mdpointed atsupportsAgyModelSelection, which no
longer exists. The CHANGELOG entries naming those gates are left alone — they
record the removal.README.zh-TW.mdwas nine passages behind, and the way it stayed behind is
worth recording: the floor commit edited it, so it read as done. It updated the
one-line requirement at the top and left everything else, which put the
prerequisites table (AGY ≥ 1.0.3, recommending 1.1.2) in direct contradiction
with the same file's opening line (AGY ≥ 1.1.12). The English README was then
swept for stale passages and the Chinese one was not, because the sweep worked
from a list of found passages rather than from a search for the removed
identifier.git grep supportsAgy -- '*.md'bounds that set in one call and
was never run. It now returns only the two sentences that say the gates are
gone, plus the CHANGELOG history.Brought in line: the feature list, the prerequisites table,
--probe-agy, the
engine-selection list, both JSON-envelope paragraphs, the Security stdin
bullet, and the AGY-transport-fallback bullet, which is deleted here as it was
in English. The prompting skill'sgemini-prompt-antipatterns.mdreference
still told the model to condition on AGY 1.1.8 for the JSON envelope; above
the floor that condition is always true.tests/readme-translation-parity.test.mjscloses the gap that let this
happen. Two assertions: the two READMEs cite the same set of version numbers,
and each one's AGY prerequisite row states the floor the code enforces, read
fromAGY_MINIMUM_VERSIONrather than from the other document. Version
numbers are the part of a page that survives translation unchanged, which is
what makes the drift mechanically visible at all. Reverting the prerequisites
row in either language turns both assertions red; drifting one version number
in one file turns only the first; moving the floor in the code turns only the
second.Two Chinese renderings are corrected while there. "per-plugin data directory"
had been carried over as 每外掛資料目錄, which is not a construction Chinese
makes; and "non-blocking limitations" as 非阻塞限制, which in Chinese is the
concurrency term rather than "these do not block you". Neither is a
translation a reader could recover the meaning from.A third correction came from asking AGY to review the Chinese README for
English-shaped prose:effortwas rendered 努力 in three places, where the
rest of the same file says 推理強度. 努力 is the everyday word for personal
exertion, so "別名與努力等級" reads as tiers of trying hard rather than as a
reasoning-effort setting. That review was run as a controlled check — the two
corrections above were re-injected into an otherwise-correct file — and it
found one of them, missed the other, and returned twenty-two items in total,
most of which were house voice it wanted flattened. Useful as a source of
candidates, not as a verdict. -
AGY turns now get ten minutes, the same as Gemini CLI. The old two-minute
cap was defending against AGY 1.0.3, whoseagy --printreturned nothing over
a pipe in non-interactive use; a ten-minute spawn against that version would
have hung silently, so the cap made it fail fast instead. The 1.1.12 floor now
refuses 1.0.3 at engine detection, and a stalled turn is visible anyway, since
1.1.8 and newer stream the envelope as they go. The cap outlived what it was
guarding.What it did in the meantime was kill work that was going to finish. A
read-only audit of three files in this repository timed out three times at the
default and completed in 223 seconds at--timeout 600— same prompt, same
scope, one variable. Narrowing the scope fourfold in token cost did not help;
only the budget did. Codex answered the identical prompt in 155 seconds, so
this is what an agentic pass over three files costs, not something slow about
AGY.The failure message pointed away from the fix, and now names it. "Retry later,
reduce prompt size or review scope, or run it on the other engine" offered
three suggestions, none of which addresses a budget that is simply too small.
It now leads with--timeout <seconds>and keeps the rest, because an
oversized prompt and a stalled engine remain real causes.--print-timeout, which AGY self-terminates on, is derived from the budget and
follows it up to 585 seconds. Neither bound on--timeoutmoved: 30 to 3600
seconds, as before. -
An unreadable job store no longer reads as "nothing to gate".
listJobs
answers[]both for a store with no jobs in it and for one it could not
read, and an empty list is exactly why the stop gate lets Stop through. A
permissions problem, a lock, or a clobbered directory therefore disarmed the
gate without a word.listJobsis unchanged. Making it throw would turn a read failure into a
crash at fourteen call sites, and for thirteen of them the merge is right — a
status listing with nothing to show reads the same either way. Only the gate
treats "no jobs" as a licence to skip, so only the gate asks the new
jobStoreUnreadableReason, and only on the path where the answer changes
anything. It still fails open; it now says why, in the same shape as the
review-failure warning beside it.ENOENT is deliberately not a failure: the directory is created on first write,
so its absence is the ordinary state of a workspace that has run no jobs yet.
That carve-out has a test of its own, added because a mutation proved nothing
else covered it — the neighbouring no-warning test seeds a job, and seeding is
what creates the directory. Without it, the new warning would fire on every
Stop for anyone who had not used the plugin yet.The unreadable case is reproduced portably by putting a file where the jobs
directory belongs: readdir answers ENOTDIR everywhere, where a chmod does
nothing on Windows. -
A degraded adversarial review now says why the engine dropped out. With
--engines gemini,agyand a sub-floor AGY, the warning readunavailable: agy.and stopped there — the refusal that namesagy updatewas caught and
reduced to the engine's name, so the one path where the user most needed the
fix was the one that withheld it. It now carries each engine's reason, in the
sameengine (reason)form the all-engines-unavailable error already used. -
A missing AGY is now reported as missing. Three different problems shared
one message, and on Windows the likeliest of them — AGY simply not installed —
was answered with a lecture about argv injection, because path resolution
throws before the friendly "not available" line is ever reached. The security
refusal now fires only whenagyresolves to something that is not an.exe,
a binary that resolves but cannot run names its path and what it printed, and
a missing one gets the install command. -
The release pipeline now checks what users receive, and says why a version
exists. Three additions to.github/workflows/release.yml, none of them
touching its permission model:- The tag must be reachable from
main, andmainmust already offer that
version. Delivery here is the default branch, not the tag: the marketplace
source ismain, so a version becomes installable the moment its bump
merges — before this workflow runs. A tag offmain, or amainwhose
manifest says something else, publishes a release nobody can install.
Nothing checked either. - Release notes carry the changelog section. The page showed only GitHub's
generated list of PR titles, so the reasons — written here — reached nobody.
The section is extracted in the job that has a checkout and handed to the
publishing job through the environment, never interpolated into its script:
whoever can push a tag writes that text, and${{ }}inside arun:body
would hand them a shell in the one job holding a token that can publish. - A new
verify-publishedjob runs the plugin from a bare clone of the
published tag — nonpm ci, nonode_modules, nothing built, which is how
Claude Code resolves a marketplace plugin — and asserts it reports its own
version. Everything before it tests the working copy the workflow was
handed; this is the only step that tests what a user gets.
- The tag must be reachable from
-
The AGY positional prompt path is gone, and with it a guard that protected
nobody.buildCliArgscarried a branch that put the prompt in argv, plus
assertAgyPromptSaferefusing a NUL byte or anything above 24,000 characters
before it did. That branch existed for AGY below 1.1.2, which the floor now
rules out — and every production caller already passeduseStdin: true, so
the guard ran for no one. Only the tests could reach it, through the same
injection seam that makes the code testable, which is why a green suite said
nothing about it.Found by asking a different question of the whole codebase: replace every
user-facing message with a sentinel, run the suite, and see which ones no
test names. Of 59 messages on the engine, setup and job paths, 28 are held by
a test. This was the first of the rest to be judged, and it was dead.The three tests that covered it are replaced by the property that made the
guard unnecessary: a prompt is never in argv, whatever it contains. The
prompt-too-longfailure category stays — an engine can still say a prompt
exceeds its context window — but itspromptNul/promptTooLongflag inputs
and theNUL byte/positional prompttext patterns go with their producer. -
The error paths nothing had ever run. The message audit was finished with a
second instrument: the whole suite underNODE_V8_COVERAGE, which records the
spawned CLI runs too, asked whichthrowsites no process reaches. 19 of
them. The sentinel pass says which messages a test asserts; coverage says
which states are ever entered, and only the second can tell dead code from an
untested live state.Judging them one by one found one more piece of dead code and one defect:
buildSingleJobSnapshotcarried a second, differently worded "No job found
for X" that could not run —matchJobReferencethrows its own first —
and if it ever had, it would have saidNo job found for "undefined",
because the only way to reach it is with no reference at all. Replaced by a
message about the store, which is what is actually true there./gemini:statusdoes not scope by session, unlike result and cancel, so its
refusal must not offer--allas a way to see more. Pinned, so that a
well-meant consistency edit cannot add advice for a problem no one has.
The other 17 were live states no test had entered: an ambiguous job prefix,
cancelling with two jobs active, reading a review group mid-run, and ten
command-line refusals that the suite could never reach because it drives the
companion through its exported functions rather than its argv.
tests/unreached-error-paths.test.mjsreaches every one of them.1 remains, knowingly: readonly-guard's TOCTOU detector, which fires when a
path is swapped between anfstatand anlstatinside one function. It has
no injection seam, and a test that pretended to reach it would be worse than
the honest note now sitting where it would have gone. -
The AGY transcript is no longer a version fallback, and
PRIVACY.mdnarrows
to match. Removing the 1.1.8 gate was going to delete
scripts/lib/agy-transcript.mjsoutright. It survived because reading the
code found a second, live reason for it that the plan had not accounted for:
when AGY is killed before it prints, the turn has still run and still been
billed, and the transcript is the only surviving copy of what it produced.
That path fires on current AGY. So the module stays, its primary-path callers
are gone, and the privacy table now says the brain directory is read in that
one case rather than "on AGY older than 1.1.8". -
A stated engine-support policy (
README.md§Versioning). The floor moves
only when a capability the plugin depends on requires it, never to keep pace
with upstream releases; when it moves it moves in a MINOR release whose first
CHANGELOG line names the new floor and the upgrade command. There is no
support window measured in time. -
Skipped tests: 8 → 6, and every remaining one runs somewhere in CI. Five
old-version tests were deleted rather than repaired, three were retargeted at a
supported AGY, and the two platform-keychain skips (macOS, Linux) were already
covered by the matching CI runners. The four still skipped on Windows are
skipped because the capturing AGY fixture needs a POSIX shebang and the plugin
refuses a.cmdshim for AGY on purpose (CVE-2024-27980) — a security
property, not a gap to close with a fixture. -
CI runs on branch pushes, not only on pull requests. Four tests in the
suite are platform-bound — two assertcmd.exeargv behaviour and run only on
Windows, two read an OS keychain and are skipped off Linux and macOS — so a
branch with no PR open was verified on whatever single platform the maintainer
happened to be on.workflow_dispatchcannot fill that gap: GitHub only
offers a manual trigger for workflows already on the default branch, which an
unmerged branch is not.mainstays excluded, because everything reaching it
has already run this workflow as a pull request. -
Five assertions that only ever ran off Windows, fixed. The first branch-push
run failed on all three platforms while the local suite was green, and each
failure was a test measuring the host rather than the code:- The POSIX AGY stand-in still announced 1.1.2, which the floor above refuses,
so three transport tests were asserting the refusal text instead of the
transport they name. Both stand-ins now sit above the floor, and the
failing-AGY failure category is one value rather than a per-platform branch. - The AGY review transport test drove its response through the transcript,
which this release removed as a response source. It now arrives in a stdout
envelope, and the transcript carries a marker asserted not to appear. detectEngine's "resolved but cannot run" case used a Windows-shaped path,
which is not absolute on POSIX, so off Windows it got the
not-an-executable refusal rather than the one under test.- The readiness test for a stale AGY beside a working gemini never injected
geminiCredentialedFn, so readiness resolved credentials from the machine
running the test: green wherever a gemini credential existed, red on every
runner. The assertion is about AGY, not about the host.
- The POSIX AGY stand-in still announced 1.1.2, which the floor above refuses,
-
Two flags a command advertised and then refused.
review.mdand
adversarial-review.mdtell the model to stop on any value not in their
allowlist, and--effortwas in neither list while sitting in both
argument-hints;--engineswas missing from the adversarial one as well,
despite being documented in the README and accepted by the runtime. A model
following the file correctly had to refuse--engines gemini,agy. Both lists
now admit them, and a test pins the property rather than the instance: every
flag a command'sargument-hintoffers must be one its allowlist admits.Found by AGY, scoring this repository against the same rubric as the
maintainer's own pass. It reported the flag as undocumented, which is wrong —
README.md:182,208document it — but the half it got right is the half that
matters, and it is not one a self-assessment was going to find. -
autono longer calls a refused AGY a missing one.detectEngine's auto
branch wrapped AGY path resolution in a barecatch, which collapsed two
opposite facts into one: AGY absent, and AGY present but refused. On Windows an
npm-installedagy.cmdresolves, is rejected on purpose (CVE-2024-27980), and
the user was then told no AGY binary was found — advised to install what they
already had. Absence is still swallowed, because for that one the existing
message is right; a refusal is now spoken, and it names why gemini is not the
way out (unauthenticated, or not installed at all) rather than offering
--engine geminion a route that only reached AGY because gemini was unusable.
Which failure occurred travels as acodeon the error rather than being
re-derived from its text.Also reported by AGY's independent scoring pass.
-
The last stage of the release pipeline drafts the announcement, and posts
nothing.scripts/build-social-announcement.mjsturns a version's changelog
section into a post sized for X (280) and Threads (500), and
.github/workflows/announce-release.ymlruns it onrelease: publishedand
writes the result to the job summary for a human to copy.It holds no credential on purpose. Automating the posting would buy back a
couple of minutes a year — this project releases a handful of times — in
exchange for two long-lived social tokens living permanently in repository
secrets. Threads is the worse of the two: its long-lived token lasts ~60 days,
so a workflow running six times a year would meet an expired one nearly every
time, and keeping it alive needs a scheduled job holding a token that can
write secrets — a higher-value credential than either social account.Highlights are not a new convention. Every changelog entry already opens with
a bold lead-in written when the change was fresh, and those are the headlines;
nothing is paraphrased or summarised, so there is no opportunity for a model
to turn a null check into an architecture. Posts that do not fit drop whole
headlines from the end rather than cutting a sentence in half.The workflow also takes a
workflow_dispatchversion input, so it can be
exercised against a release that already exists instead of having its first
run be a real one — the failure moderelease.yml's own new jobs are still
exposed to.Nothing reads the release body: the text comes from the changelog in the
checkout, which is reviewed content. A new test applies that rule to every
workflow — no${{ }}inside anyrun:body, because a tag name or release
title is written by whoever can push a tag, and interpolation makes it shell
source. Mutation-checked in both directions. -
The release pipeline was rehearsed end to end, and it found two things.
release.yml's new jobs only run on av*tag, so until now their first
execution would have been a real release. A throwaway private repository was
loaded with this tree, bumped, and tagged, which ran the whole workflow for
real:verifypassed all of its gates,publishcreated a release whose body
carries the changelog section above GitHub's generated notes, and
verify-publishedfailed only because an unauthenticated clone cannot read a
private repository — that job's script was checked separately against the real,
publicv0.24.4and reports the right version.What the rehearsal caught:
- A minor bump must also update three documents, or
npm testfails inside
release.ymlafter the tag is pushed:SECURITY.md's supported line,
PRIVACY.md's "Applies to plugin version" line, and the version cell in
docs/COMPARISON.md. The tests that enforce this are deliberate — the
privacy document once claimed 0.16.x for six releases — but nothing said so
where someone cutting a release would read it. - The announcement test was itself a release blocker. It required three
headlines in the newest changelog section; a patch release with one entry
would have failed it, at the same point in the pipeline. It now requires one.
Found by a dry run with a one-entry section, which is exactly the release it
would have blocked.
- A minor bump must also update three documents, or
-
The stop gate no longer treats a review that reviewed nothing as a pass.
The hook guarded its review result withif (!payload), which asks whether the
process returned anything, not whether it returned a verdict. Two ways to exit
0 with no verdict slipped through, and both spent the gate's one-shot
gateReviewedAtmark and let Stop past in silence — the edits were recorded as
reviewed forever, unseen.The first is a change that was committed before Stop fired. The gate reviews
--scope working-treebecause the plugin never commits, but nothing stops the
agent or the user from committing first, and then the companion returns
{ empty: true, result: null }with exit 0. The second is a turn that
succeeded while the model wrote prose instead of the structured review:
resultis null and the exit status stays 0.The guard is now
if (!payload?.result?.verdict), so both land in the
fail-open branch that already existed — Stop is still allowed, the mark is not
spent, and the skip is visible. That branch's own wording was widened to match:
it claimed the review "could not run", which an empty scope did not.The hole was in the one direction this file had already ruled out. The same
function chose--scope working-treedeliberately, "instead of relying on auto
scope (which could resolve to an empty branch diff and pass vacuously)" — the
vacuous pass was anticipated for branch scope and closed there, while
working-tree kept the identical shape. And the visible-warning design at the
fail-open branch says the skip must never be silent; this was the one route
that was.Both routes now have an end-to-end test through the real hook, asserting on the
stored job that the mark is unspent. The prose route needed a fixture that did
not exist:review-proseintests/fixtures/fake-gemini.cjsis a turn that
succeeds and answers in prose, which is the shape the engines actually produce
when they answer the prompt instead of obeying its output contract.
What's Changed
- The stop gate treated a review that reviewed nothing as a pass by @arcobaleno64 in #155
- Declare an AGY version floor, and delete the seven gates it replaces by @arcobaleno64 in #154
- An unreadable job store read as "nothing to gate" by @arcobaleno64 in #157
- fix(agy): give an AGY turn the same ten minutes gemini gets by @arcobaleno64 in #158
- chore(release): 0.25.0 by @arcobaleno64 in #159
Full Changelog: v0.24.4...v0.25.0