github feder-cr/AIHawk v0.67.0
0.67.0

latest release: v0.68.0
one hour ago

An audit for code that exists only because the tests grew up around it. Seven
surfaces came out of src/aihawk and every one had the same shape: zero callers
in the product, and enough callers in the suite to look load-bearing.

AGENT.RUN_TASK WAS A SECOND WAY TO RUN THE AGENT LOOP. Its own docstring said
so: "not called by the product - kept because the suite drives the loop through
it, about twenty-five tests". Worse than unused, it took an object with
list_tools and call_tool, which is the shape from before Link existed, while the
product passes link.call and link.tools to Conversation.run. A reader of
agent.py met two entry points with two different ideas of how a tool is reached,
and one of them was imaginary. The convenience was real, so it moved to
tests/_loop.py rather than being deleted. The e2e test that drove a real server
through it now goes through Conversation.run, which is the path the product
takes.

SESSIONS.AROUND OFFERED ITSELF TO "ANYTHING EMBEDDING THIS", a user that does
not exist: this is an application, and the only importer of sessions.py is
cli.py. Eleven callers, all in two test modules. The argument it was written for
is good and survives in tests/_sessions.py, because it is a rule about the
suite: a test driving ONE conversation should still go through build_app and
the routes, so the single case is exercised by the same code the many case uses.

SESSIONS._OPEN_LINK WAS A SEAM NOTHING DECLARED. It was assigned in init and
reassigned from outside by two test modules - a convention a reader of the
signature could not see and a reader of the assignment could not tell from an
accident. It is a keyword argument now, so how a connection is made is part of
the interface and the default is the product's one way.

SESSIONPLAN.DESCRIBE WAS A SECOND MAPPING OF A PLAN ONTO THE SENTENCE a caller
is told, beside the one in work.open, and the two were free to disagree the day
a fifth field joined the sentence. It also described something that does not
happen: between planning and launching, work.open can rewrite the settings and
the exit note - that is how the helper browser comes to share main's exit - so
the sentence is read from what was LAUNCHED and never from what was planned. A
method on the plan quietly offered the other thing. Its six callers were all in
one test file and now go through the function the product goes through.

CLEAN.RELEVANCE AND CLEAN.CLEAN_STATS ARRIVED DEAD and stayed that way. Fifty
seven lines scoring how much a model needs an element, and seven figures about
what the cleaning saved, both imported with the server on 2026-09-06 and named
by no commit since. clean_stats was in all and had a test; relevance had
neither, and the module docstring described it as part of how the file works -
"the relevance score below orders and annotates" - so the file's own account of
itself named a pass that does not run. What those two were really carrying is
kept: that nothing here may drop an element, and that a reduction figure says
nothing about whether the result is still usable. Both are the invariant at the
top of the module, which is where a rule can act.

CLEAN.BLOCK_TAGS was a tuple nothing read.

TWO PLACES ALSO STOPPED HOLDING THE SAME FACT TWICE. storage.file_for owns the
rule that turns a session id into a path, so chats.py and mcp/store.py each name
only their own directory and neither spells the join. And a test migrated off
the store.home re-export onto aihawk.storage.home, which is where home lives.

THE GATE IS ON THE CLASS, NOT ON THE SEVEN NAMES. A list would stop those seven;
the defect is that a surface can be added, be used only by its own tests, and
look exactly like one the product depends on. So it asks the general question:
every top-level function, class and constant in src/aihawk must be named
somewhere in src/aihawk. Being registered excuses it, and that is structural - a
decorator spelled .tool, .command or .group hands the object to FastMCP or to
click, where the call happens over a wire or from a shell. Wrapping does not:
dataclass and contextmanager hand the object straight back, so it is still
called by name. Exporting it in all does not count either, because all
is the claim under audit rather than evidence for it, and that is precisely
where clean_stats was hiding.

WHAT THE GATE DOES NOT SEE IS WRITTEN INTO IT. 178 definitions across 25
modules, with floors asserted so a scan that goes blind fails instead of
printing the same clean line. Methods are out of scope: a method reaches the
code as an attribute, and plan.describe and SessionPlan.describe are the same
attribute name, so no scan can tell the dead one from the live one beside it.
That one was found by reading, and this gate would not have found it.

Suite 654 green, from a 642 baseline: one test went with clean_stats and
thirteen arrived with the gate. Six known-bad inputs and five cases that must
not fire, plus a mutation against the real tree - relevance put back into
clean.py, which the gate names by file and line. The first three must-not-fire
cases failed on the first run and the gate was right: a toy module whose own
outermost function has no caller has a dead surface, so each fixture ends at a
registered entry point, exactly as the package does.

Don't miss a new AIHawk release

NewReleases is sending notifications on new releases.