Releases: muratovv/ai-hats
Release list
v0.15.0
[0.15.0] - 2026-08-31
Fixed
- The published library was missing the module the consent wrapper imports (HATS-1876).
core/skills/safety-guard/hooks/consent_gateis a symlink to the realhooks/consent_gate, and the sdist build recorded those six files once — under the symlink — and dropped the directory they actually live in. The wheel is built from that sdist, soai_hats_library.hooks.consent_gatewas absent from every published wheel,0.5.7included. Nothing noticed while no installed code imported it;ai_hats.consent_wrapperis new in this release and imports it at module level, so a freshpip installwould have raisedModuleNotFoundErroron the first HITL session that composed a role declaringapps.consent_gate. Measured against the real artefact rather than the build: the wheel downloaded from PyPI carries sixconsent_gatefiles under the symlinked path and none under the real one, and installing it into a clean venv reproduces the import error. The sdist now names the real directory; the wheel picks it up on its own, which is why only the sdist declares it — adding it to both makes hatchling refuse the duplicate.
Added
- A knob you could only learn about by reading the function that reads it (HATS-1872).
docs/how-to-configure.md, the page a person configures a project from, named exactly one of the thirty configurable environment variables, and fourteen of them appeared nowhere indocs/at all.docs/reference-env.mdnow carries all thirty — budgets, our own path overrides, and the seven homes we honour but do not define — each with its type, its default and a line saying what it does. The page is rendered from the declarations rather than written beside them, and a CI stage refuses it once it falls behind, the waytests/e2e/CATALOG.mdalready worked. What that precedent cannot do, and this one must: for a number, a page that merely agrees with the code is a second home that drifts, so the declaration is the default and the reader takes it from there. The four budgets belonging to hooks the library delivers into a consuming project keep no declaration — those hooks run whereai_hatsis not importable — so the generator reads their defaults out of the call site itself. A companion guard refuses any name shipped code reads that nothing declares, across both languages and every distribution.
Fixed
-
One of six readers of the same knob crashed instead of falling back (HATS-1872). Six modules each carried their own copy of "read a number from the environment, fall back when it is unusable", and five of them documented that contract in a docstring. The sixth, the pipeline-run rotation, parsed with a bare
int():AI_HATS_PIPELINE_KEEP_N=abcraisedValueErrorand took the harness down, where every sibling would have returned its default. The contract was real and written nowhere, so nothing could notice the one that broke it. All six now read through one declaration that also holds the number, and the contract is a test over every budget rather than a sentence repeated five times. -
A bypass flag a sub-agent could set for itself (HATS-1872). The launcher withholds approval flags from sub-agents by recognising the shape of the name — an
_ACK,_OFFor_SKIPsuffix.AI_HATS_SKIP_SELF_LOCATION_GUARDcarries its verb at the front instead, so it went through, and it disables the self-location guard outright. Widening the shape rule was measured and rejected rather than assumed: a front verb has no polarity, and the same widening would also blankAI_HATS_SKIP_RETIRED_PRUNE, handing a child theuv pip uninstallits parent had suppressed. Only the guard hatch is an approval, so it is named explicitly besideAI_HATS_YOLO, which the predicate already named that way; a scan now refuses any future front-verb flag until someone rules on it. -
Every tool call paid 105 ms to import a dispatcher (HATS-1869). The hook dispatcher is a fresh process on each tool call, on all five surfaces — including opencode's hookless roles, where it spawns and finds no rows, and claude, whose dispatcher is spelled
channeland arrived with HATS-1868 while this card sat in review. Three package__init__files on its path each pulled the same heavy subgraph:ai_hats.surfacesandai_hats.surfaces.<surface>reached the schema layer through the surface contract, andai_hats_corecost pydantic, filelock and asyncio to reachDeadline, which imports nothing but stdlib. Measured, not inferred, and the reason the obvious fix was not enough: the three edges are independent, so cutting any two of them still left over 80% of the cost — and the two fixes that suggest themselves, movingDeadlineout of the core package and giving the dispatchers an entry point outside the surfaces package, are two of those three cuts, so together they would not have fixed it. Only all three moved the number, from ~105 ms of import to ~20 ms (wall ~118 ms → ~34 ms per dispatcher, same interpreter, back to back). All seven facades now bind their exports lazily (PEP 562), the wayai_hats_observealready did; every exported name resolves as before, the deprecatedProvideraliases included. One shape does change: a bareimport ai_hats_coreno longer exposesai_hats_core.locksand its six sibling submodules as attributes, because nothing imports them for it any more.from ai_hats_core import locksstill works, and the package's documented API —__all__plussafe_delete— is untouched. Because one eager import anywhere on that path silently restores the whole cost,tests/test_dispatcher_import_closure.pyrefuses a dispatcher whose import closure contains any of them. One side effect is worth naming, because it narrows HATS-1858's "an undeliverable gate refuses": a dispatcher that could not import its own dependencies used to exit non-zero, which agy reads as BROKE, so a half-installed venv refused every tool call — with no reason text and without namingAI_HATS_GATE_BROKEN_ACK. The dispatcher no longer imports enough to notice, so on an event whose manifest has no rows a broken install now passes. Where a gate does run, the refusal still lands, one level down and better worded — it carries the traceback and the hatch. -
The delivery hatch was named by every refusal and read by nothing (HATS-1858).
AI_HATS_GATE_BROKEN_ACKappeared in the text of every refusal this channel imposes — "set it to proceed past this gate" — and no code anywhere consulted it. The git channel states the invariant atgithooks_run._skip_reason: a deny that names a flag nobody reads is worth nothing (HATS-1253 P4). Half of it had been ported. This landed on the same change that converted nine cline branches from fail-open to fail-closed, so a session whose manifest went missing refused every guarded tool call with no way out short of disabling ai-hats. The flag is now read where the refusal is formed, at both levels a gate can fail to be delivered, and taking it is recorded on stderr and in the bypass journal rather than passed in silence. -
agy ran the whole gate chain on events nothing can bind to (HATS-1858). Its global hook registers five events and only two are bindable; the other three fell through to
PreToolUse, and a payload carrying no tool name makes the matcher run every row — so eachNotification, several per turn, putsafety_gate.py,pre_bash_shared_state_guard.shandbacklog_write_gate.pythrough a call that does not exist. The same collapse keyed the user's own channel wrong, so theirStop,NotificationandPostInvocationhooks stopped running entirely while theirPreToolUsehooks fired on every notification. Codex has carried the guard this mirrors since its own dispatcher was written. -
A manifest that never resolved met four different answers (HATS-1858). agy passed the call with a line on stderr, cline refused and named the hatch, codex wrote a bare status naming nothing, and the OpenCode plugin disabled every gate for the whole session by returning no hooks at load time. All four now refuse through one constructor, name the hatch and honour it. This reverses HATS-1339's deliberate fail-open for the vanished-manifest case on agy, which HATS-1439 is the cost of; the reversal is only defensible because the hatch above now works, and its e2e asserts both halves.
-
OpenCode ran whatever command the manifest named (HATS-1858). codex and cline have both refused a hook command outside the session skills mirror, or without the executable bit, since their dispatchers were written; the OpenCode dispatcher checked neither.
SurfaceProfilehad carried the mirror root the whole time and had no callers at all. -
Every hook's stderr but the decider's was dropped (HATS-1858). An allowing hook is the one whose stderr is its only trace —
bypass_journalwrites its own "NOT RECORDED" warning there — so a hatch could be used with the record lost and the warning swallowed. The codex dispatcher had forwarded this unconditionally under a comment saying that swallowing it would erase the audit trail; the line went missing in the move onto the channel. -
OpenCode's file gates matched the tool and inspected nothing (HATS-1858). OpenCode names the argument
filePath; every shipped file gate readsfile_pathorpathand ALLOWS the call when neither is present (wt_gate.py). The surface's profile declared no argument renames at all, soread,editandwritereached the gates with a key none of them look at. The name is observed rather than guessed — opencode's own session database recordsfilePathon all 154 reads, edits and writes it holds, whilegrepandglobsendpath, which the gates already read — the same kind of evidence that produced codex'sexec. -
Codex ran no terminal gate at all (HATS-1858). Codex names its shell
exec; the tool-name table insrc/ai_hats/surfaces/codex/claude_hook_adapter.pyhad no termi...
v0.14.0
[0.14.0] - 2026-07-29
Removed
-
The legacy
ai-hats taskCLI is unmounted (HATS-1260, epic HATS-1159).
The four command groups —task,task hyp,task proposal,task attach
(28 verbs) — are gone from theai-hatssurface;rackis the only backlog
CLI. Migration:docs/migration-v0.14.0.md(ships with the release). An
unrecognized leading word now follows the standard bare-positional-prompt
rule (HATS-087 covered flag-shaped tokens; bare words became passthrough in
HATS-1202, in this same release), soai-hats task …no longer errors — it
starts a session with that text as the prompt. Recorded behavior change:
task_prefix
auto-detection from pre-existing task folders (with persist-back to
ai-hats.yaml) was a feature of the removed CLI path; rack reads
task_prefixfromai-hats.yamlonly — legacy projects should set it
explicitly. -
The
backlog-managerskill tree and its git hook are deleted (HATS-1261,
epic HATS-1252). Oncehatrackbecame the default manager (HATS-1054) the
classic skill was composed by no role and taught a CLI that no longer exists.
The skill, its five reference files and thepre-commit-attachments.shhook
that guarded the legacy card subtree are gone fromai-hats-library;
hatrackis the only shipped backlog-manager skill. Migration:
docs/migration-v0.14.0.md. -
packages/ai-hats-trackeris deleted (HATS-1262, epic HATS-1252). The
package backing the retiredai-hats taskCLI is gone from the uv workspace,
the root dependency set, and the publish workflow. Its surviving consumers
were re-homed first — the ownership registry,linked_contextand
TrackerPathsintosrc/ai_hats(HATS-1258), and the retro window onto the
rack facade (HATS-1259). Migration:docs/migration-v0.14.0.md. An existing
venv keeps the orphaned distribution — and with it a working legacy CLI over
the same store — until it is pruned; seeself updateunder Fixed.
Fixed
-
A no-op self-heal no longer deadlocks the CLI in an infinite re-exec loop
(HATS-1359).bootstrap_or_die()treatedattempt_self_heal()'s exit code as
proof the missing dependency was importable, thenos.execv'd
unconditionally. Butuv pip install <bare-name>can exit 0 by auditing an
existing dist-info as already-satisfied without the module ever becoming
importable — so the re-exec'd process found the identical dep missing and
looped forever, until Ctrl-C. Becausecli.main()calls it before subcommand
dispatch, evenai-hats self update— the in-band fix — hung, leaving no
escape but an out-of-banduvcommand. It now rechecks
find_missing_runtime_deps()before re-execing (the pattern
verify_after_install()already used one function over) and fails loud with
the rescue command instead of looping. This release can trigger the
condition: deletingai-hats-tracker(above) leaves an editable install
whose metadata predates the removal still declaring a dependency whose source
directory is gone. Migration:docs/migration-v0.14.0.md§6. -
ai-hats self updateprunes distributions the new version retired
(HATS-1280).self updateinstalls, it does not synchronize: a dependency the
new version dropped stayed in the venv with its console scripts. After 0.14.0
that would leaveai-hats-trackerinstalled alongside — a working legacy
backlog CLI over the same store, with a diverging plan-section catalog and no
edge:bindings — so the exact hazard the cutover exists to remove would
survive the upgrade under a different name. The prune runs post-install in the
new interpreter, so it fires on the upgrade that introduces it rather than
one release later, and works from an explicit retired-distribution list rather
than generic orphan detection. No-op on the managed blue-green path, stands
down on editable installs, and fails open whenuvis unavailable.
Migration:docs/migration-v0.14.0.md. -
wt mergerefuses a stale ref and never deletes a branch it did not land
(HATS-1346). A live incident dropped two commits: the auto-merge on
review → doneconsumed an integration ref prepared by an earlier session
and fast-forwarded that, then removed the worktree and deleted the branch —
whose tip was two commits ahead. The work survived only because the objects
were still unreachable-but-present in the shared object store, onegit gc
from gone.mergenow resolves the task branch tip at merge time; a
caller-suppliedexpected_tipthat no longer matches is a typed
WorktreeStaleRefErrornaming both SHAs, and teardown is gated on containment
(merge-base --is-ancestor <tip> <target>) rather than on the merge step
having returned zero — a target that does not contain the tip raises
WorktreeMergeIncompleteErrorand leaves the worktree and branch in place.
Both are precondition refusals, so neither is reported as a failed merge. Same
defect class as HATS-1307 — validating against cached state instead of live
state — this time on the destructive path, where thereview → executerework
loop makes "another session advanced the branch" a normal condition. -
A freshly created worktree comes with its own venv (HATS-1291). A
rack-created worktree had none, and thegit-masterypre-commit smoke hook
resolvedpytestthroughPATH— landing on the main checkout's interpreter,
which the wrong-checkout guard then refused. The very firstgit commitinside
a new worktree failed, and the printed remedy was a ten-line manual
provisioning recipe. A newworktree-venvskill contributes awt_inhook
that provisions the venv at worktree creation; the smoke hook now runs the
committed checkout's own pytest. -
The pre-commit smoke hook stops blocking commits in projects without
tests/e2e/(HATS-1352). The hook is shipped to consumers through the
git-masteryskill, and it passedtests/e2e/to pytest unconditionally.
pytest answers a missing path with rc=4 (usage error) — not the rc=5 the hook
treats as "nothing to run" — so any consumer project with anintegration-
tagged task inexecuteand no such directory had every commit blocked.
The path is now passed only when it exists; without it pytest falls back to the
configuredtestpaths, which is the pre-scoping behaviour. -
ai-hats-agyis published (HATS-1353). Theagysurface was listed in
KNOWN_SURFACESand self-heal ranuv pip install ai-hats-agyfor it, but
nothing ever built or published the distribution — so selecting the surface on
a stable-channel install ended in a missing package. It now builds and
publishes fromrelease-packages.ymlin its ownpypi-agyenvironment, and a
test pins that every surface in the registry has a publish job. -
The
ai-hatsbinary runs from inside a worktree (HATS-1306). The launcher
resolved the project venv relative to cwd, so any invocation inside a linked
worktree died withvenv missing at <worktree>/.agent/ai-hats/.venv. That
killedai-hats wt exec <branch> -- git commitoutright and every hook or
script that re-enters ai-hats from inside a worktree. The launcher now hops to
the main checkout, guarded on that root actually carrying.agent/or
ai-hats.yaml. -
rack transition --append <field>=<json>can no longer render a card
unreadable (HATS-1299).--append tags='["x"]'nested the array as a single
entry, and the card then failed strict validation on read:rack context
reportedTask not found,--setcould not repair it because it validates
before it mutates, and the only way out was hand-editingtask.yaml. Reads are
now tolerant and writes strict:from_yamlcoerces stray entries and reports
them as warnings,saverefuses any mapping the strict model cannot load back,
and an array extends rather than nests. -
Link, field and document ops reach the audit journal, on both sides of a
link (HATS-1351). Only state transitions andepicifyever reached
audit.jsonl— there were zero records for--link,--unlink,--set,
--append,--logor the document ops. A re-parent left no trace at all, and
the mirrored side learned nothing.transitionnow emitsop:*records
carrying the field or document name, and the post-lock mirror delta is
persisted on the target card rather than dropped. -
A second
foldis refused instead of silently overwriting the first
(HATS-1328).--link fold:<ID>on an already-folded card overwrote the scalar
link field, so the first fold vanished — silent loss of exactly the audit trail
folding exists to leave. It is now a typedalready_foldedrefusal naming the
current target, andfolded_intodeclares the derived inversesubsumes, so
"what was folded into this card" is answerable again. -
The documented re-parent command is the one that works (HATS-1350). The
hatrackskill anddocs/how-to-hatrack.mdboth taught
rack transition <ID> --set parent_task=<EPIC>, which rack refuses, and the
refusal's own suggestion then failedalready_linkedbecause the field was
occupied — an agent following the docs hit two typed refusals in a row. Both
now show the working form:--unlink parent_task:<old> --link parent_task:<new>in one transition. -
The drift guard no longer dead-ends
rack transition <id> done(HATS-1307).
Drift was measured against the base SHA snapshotted atwt create, so a branch
rebased onto the moved base was still refused with "N commits ahead" — and the
only override,--accept-drift, lives onai-hats wt merge, which
rack transitioncannot pass. Drift now also asks whether the branch already
contains the base, so the rebase every operator reaches for first is what
actually clears it. Both refusal recipes lead with that rebase and demote
--accept-driftto what it always meant: mergin...
v0.13.2
[0.13.2] - 2026-07-10
Added
- Provider open-registry + entry-points IoC seam (HATS-870, T10). The closed
PROVIDERSdict is now an open registry: built-ins self-register at import and
third parties register viaregister_provider()or theai_hats.providers
entry-point group — ai-hats discovers and registers an out-of-tree provider
without importing its package (a broken or duplicate entry point is warned and
skipped, never fatal).get_provider()behaviour is unchanged. Extracting the
built-in providers into their own packages stays a separate future arc
(providers remain integrator-bound per ADR-0014 P0 #4). - Cline surface plugin (HATS-956) —
ai-hats-cline, the first in-tree
consumer of the provider IoC seam, registers theclineCLI as a provider via
theai_hats.providersentry point (ai-hats -p cline). Lives under the new
packages/surfaces/category; ADR-0014 gains a surface tier that may
depend up on the integrator, enforced by the workspace-boundary lint. Inline
-srole delivery, interactive TUI for HITL, headless--yolo --jsonfor the
automate path. A transcript parser (ClineParser) and native.cline/skills/
materialization landed as follow-ups — see theai-hats-clinechangelog.
Fixed
-
Unknown
--providerfails friendly, not with a traceback (HATS-965).
ai-hats -p <unknown>now reports the bad name and lists the available
providers instead of surfacing an uncaughtValueError. -
Worktree-isolation gate no longer fires on unrelated repos (HATS-959). The
wt_gate.pyPreToolUse guard classified the edited file's own repository, so
an Edit/Write to a tracked file in a different repo than the session — e.g.
~/dotfiles/.claude/settings.json— was hard-denied, and the recovery text told
the agent to branch that unrelated repo. The gate now scopes to the session's own
repository (keyed on the payloadcwd's--git-common-dir, shared across a
repo's main checkout and its linked worktrees): a file in a different repo is
silent, while same-repo main-checkout edits — including editing main from inside a
linked worktree — still deny. An unresolvablecwd(absent / non-git) falls back
to the prior location-only behaviour, so scoping only ever suppresses a deny,
never adds one. -
ai-hats-wt 0.3.0 + integrator pin
>=0.3.0(HATS-942 drift). The
configurable base/merge-target work grew theai_hats_wtpublic surface
(get_default_base_branch,get_default_merge_branch) and edited
locks.py/manager.pyafter 0.2.1 shipped to PyPI, without a bump —
caught by the HATS-921 drift guard, which resolvers would otherwise have
ignored while serving fresh installs the stale 0.2.1 wheel. Minor bump
(new public API); publish rides the release flow. -
ai-hats-core 0.4.1 + integrator pin
>=0.4.1, published-version drift
guard (HATS-921).safe_delete.pywas patched twice after 0.4.0 shipped to
PyPI (concurrent-discard idempotency, unique-tmp atomic write) without a bump,
so resolvers preferred the stale equal-version index wheel over the fresh
local build and served fresh installs code missing both fixes. The patch bump
moves the local source past the published wheel; a new drift-guard test
(tests/test_package_version_drift.py) byte-compares every published
packages/*version against local source and fails on unbumped drift. Until
0.4.1 is published, fresh installs fail loud ("no matching distribution") —
deliberate interim (publish rides the release flow). -
Marker-less pre-marker
.claude/skills/mirror now auto-heals (HATS-931).
A stale project-scope skills mirror written by a pre-marker ai-hats version
(no.ai-hats-managedmarker) used to warn about a double skill registration
every session with no way to clear it — the auto-heal was gated on the marker.
Session start now treats any project-scope.claude/skills/<name>that
collides with a composed skill as ai-hats-owned (project.claude/skillsis
not a user-authoring surface) and sweeps it to the recoverable trash with a
heal NOTE. Home-scope collisions (~/.claude/skills) are still only warned
about, never touched (HATS-465).
v0.13.1
[0.13.1] - 2026-07-06
Added
- Worktree lifecycle effects recorded in the task card (HATS-866).
ai-hats wt create/merge/discardnow append a structured effect line
to the card'swork_log/(branch, worktree path, merge SHA), so the tracker
carries the worktree history. Routed through aWorktreeEffectsseam that
decouplesstatefromwt. - Owner registry + unclaimed-marker sweeper (HATS-905 phase 1, HATS-910).
Every mechanism materializing files outside<ai_hats_dir>registers an
owner_keyin the open registry (ai_hats.owners); onself init/bumpa
generic sweeper (ai_hats.sweeper) reclaims artifacts whose colocated marker
names an unregistered (dead) owner — the HATS-901 forgotten-migration class
is now healed by the engine. Deletion requires content proof (hash recorded
in the marker or an embedded ownership string); user-edited files are left
in place with a WARN. Gated off under version skew and hard-delete mode
(AI_HATS_TRASH_DIR=-), never runs on session-start/set_role. The legacy
.claude/publish and skills-mirror cleanups now ride the same shared
procedures (skills-export,claude-publishowners), and the publish
manifest path gained the HATS-907 traversal guard. - Hashed
owner_keymarker convention (HATS-905 phase 2, HATS-911).
Line-manifest markers are written viaai_hats.sweeper.write_marker: an
# ai-hats-owner: <key>header plus a<sha256-12> <relpath>content
hash per entry — the sweep-time proof that an entry is still engine-owned.
The live.githooks/.ai-hats-manifestnow uses this format (readers accept
both; old hash-less manifests converge on the next rematerialization). A
coverage test pins every mechanism materializing outside<ai_hats_dir>
to a registered owner; sweep liveness no longer depends on import order,
and a crashing legacy sweep procedure defers with a WARN instead of
aborting the bump.
Fixed
- Concurrent
ai-hats.yaml/customizations.yamlwriters no longer lose
each other's changes (HATS-526). Every config writer (customize,config set,init/bump, session-startset_role, relocate, feedback) loaded
the file at command start, mutated and saved the whole object — any
concurrent write since the load was silently dropped (3 parallel
customize --add-trait --globalkept 1 of 3). Writes now go through
locked_update: a cross-processfile_lock(newai_hats_coreprimitive,
filelock-backed) around a fresh re-read plus only the caller's field
delta. Contention past 10s exits with a friendly error instead of hanging;
a static guard test keeps whole-object saves from coming back. - Worktree runs and sub-agents resolve the workspace packages (HATS-913).
ai-hats wt execand the worktree env threadpackages/*/src(ai_hats_core,
ai_hats_wt) intoPYTHONPATH, so code run inside a linked worktree imports the
worktree's own workspace sources instead of the main checkout's (or failing to
import them). - A fresh
pip install ai-hatscan no longer resolve stale workspace
subpackages (HATS-923, HATS-928).ai-hats-corepublished at 0.3.0 (adds the
file_lock/LockTimeoutErrorRMW-lock helper) andai-hats-wtat 0.2.1
(addsWorktreeHook/parse_worktree_carry); the prior 0.2.0 / 0.1.0
releases lacked these symbols, so a subprocess importing them raised
ImportError. The integrator now floor-pinsai-hats-core>=0.3.0and
ai-hats-wt>=0.2.1.
v0.12.0
[0.12.0] - 2026-07-02
Added
- Standalone
ai-hats-core+ai-hats-wtpackages (HATS-885). The atomic
filesystem-I/O core primitives and the hook-agnostic git-worktree engine are
extracted into two independently-versioned PyPI packages;ai-hatsnow depends
on them (ai-hats-core>=0.1.0,ai-hats-wt>=0.1.0) andai-hats self update
pulls them transparently. The worktree engine is importable standalone as
ai_hats_wt(WorktreeManager+ the L1–L4 lock model) against a bare git repo
with zero ai-hats config. - Tool-call-hygiene
PreToolUseguard (HATS-632). Thetool-call-hygiene
skill now ships a non-blockingPreToolUseBash runtime hook: when a command
is a pure invocation ofgrep/find/cat/sed -i/… that a dedicated tool
covers, it injects anadditionalContextnudge toward Grep/Glob/Read/Edit
without blocking the command or prompting the user. Conservative by design —
any pipe / redirect / chained command is left alone. Kill switch:
AI_HATS_TOOL_HYGIENE_OFF=1. This is the first in-libraryruntime_hooks
consumer, setting the sharedstdin tool_input → JSON hookSpecificOutput
convention for the behavior-hook family. - Python security-lint
PostToolUsehook (HATS-660). A newpy-security-lint
skill (composed by thedev::pythontrait) runsruff check --isolated --select S
(flake8-bandit security rules) on every.pyyou Edit/Write and forwards any
findings to the agent via a non-blockingadditionalContextnote — an early,
edit-time security layer that complements (does not replace) the project's CI
lint. Zero egress, fail-open whenruffis absent. Kill switch:
AI_HATS_SECURITY_LINT_OFF=1.
v0.11.0 — worktree lifecycle & merge robustness
Headline: epic HATS-835 — worktree lifecycle & merge robustness. A sweep
that hardens the git-worktree lifecycle (create → merge → teardown → tracker
consistency) against the failure modes that silently lost or corrupted state.
Fixed
task transition donetolerates an already-merged, state-lost branch
(HATS-697). When work shipped on the base out-of-band (manualgit merge --no-ff task/<id>) and/or the auto-worktree was removed by hand,done
refused with a falseworktree state lost("un-merged commits") even though
the branch was fully integrated. It now detects the already-merged branch,
finalizes without a re-merge, and cleans up the stale ref; only a genuinely
divergent branch still refuses (the silent-data-loss guard stays intact).- Forced
executespins no fresh worktree (HATS-697).transition execute --forceis a manual state correction; it no longer creates a worktree off
HEAD that orphaned retrospective shipped-on-master work in the main tree. - In-worktree
transition done/closeis refused before teardown
(HATS-788). Running it from inside the task's own linked worktree used to
delete the cwd and leave the CLI resolving a phantom tracker (falsetask not found); it now refuses with guidance and preserves the worktree. - No phantom tracker on a wrong-but-alive root (HATS-839).
<ai_hats_dir>
is no longer created unconditionally, which had resurrected a phantom
.agent/tracker and drove the HATS-788 id-collision. - Worktree-adopt short-circuit works from inside a worktree (HATS-840).
The HATS-060 adopt path no longer no-ops on a hopped_project_dir, so it
adopts the caller's worktree instead of spinning a fresh one off main. - Typed refusal for
original_branch: null(HATS-714).wt merge/
task transition doneraise an "incomplete worktree state" error naming the
field instead of an opaqueTypeError. execute --batchwithout--rolefails cleanly (HATS-827), instead of
crashing on an invalidagent//<session>worktree branch.
Added
- Capstone e2e matrix
test_worktree_lifecycle_robustness_matrix.pyasserting
the epic's invariants hold together on the real launcher + binary.
v0.10.0
[0.10.0] - 2026-06-20
Added
- Self-location guard + out-of-band recovery + stray-shadow detector
(HATS-791, child of HATS-786). Closes the residual "shadow" case HATS-790's
generator removal left open: a stale ai-hats running from a FOREIGN
(non-managed) venv reached ahead of the host launcher. A pure classifier
ai_hats.self_location.classify_invocation("sanctioned"/"foreign"),
wired by_guard_self_locationintomain_entry, refuses-and-instructs
on a foreign invocation — printsremediation_text(run the host launcher /
re-bootstrap / uninstall from the offending venv) to stderr and exits 3. It
biases HARD toward fail-open (only a positively-identified foreign venv that
ACTUALLY EXISTS as a resolvable managed venv is refused; every ambiguity,
editable dev clone, or--version/--help/--treeinfo command resolves to
sanctioned), is wired intomain_entry(not themainclick group, so
in-processCliRunnertests bypass it), and has an escape hatch
AI_HATS_SKIP_SELF_LOCATION_GUARD=1(SKIP_ENV_VAR).scripts/bootstrap.sh
becomes the canonical out-of-band recovery hatch — paradox-immune because
it is fetched fresh (curl … | bash) and drives the launcher by ABSOLUTE path
("$LAUNCHER_DEST"), so a shadow cannot intercept it — with a new--repair
flag that force-reinstalls the launcher + the framework-managed default venv
(.agent/ai-hats/.venv+versions/, never a user override). Both
bootstrap.sh(detect_stray_launchers) andai_hats.cli.maintenance
(find_stray_launchers) scan$PATHfor strayai-hatsbinaries outside the
sanctioned launcher and WARN — never delete. - Forward-safe
ai-hats.yamlreader — preserve unknowns, fail loud on a newer
schema (HATS-792, child of HATS-786).ProjectConfignow round-trips a
same-version unknown top-level field instead of dropping it:from_yaml
stashes the pre-stripped unknown keys on an_extraPrivateAttrand
to_dictmerges them back (mirrorsTaskCard.extras), so an OLDER ai-hats
preserves (does not silently delete onsave()) a field a NEWER ai-hats wrote
without aschema_versionbump — while the HATS-581 stderr WARN still fires.
A genuinely newer schema fails loud:from_yamlraisesProjectConfigError
pointing atai-hats self updatewhen on-diskschema_versionexceeds
KNOWN_SCHEMA_VERSION(4), and a matchingsave()clobber guard refuses to
overwrite a file whose on-disk schema is newer than this binary knows.
Removed
- Migration: see
docs/migration-v0.10.0.mdfor
the one-time crossover (reinstall the host launcher; clear stray app-venv
installs). Removed theai-hatsconsole-script entry point;python -m ai_hatsis now the sole package entry (HATS-790, Alt 5). The[project.scripts] ai-hats = "ai_hats.cli:main_entry"generator made every venv depending onai-hats
materialise abin/ai-hatsthat direnv could prepend ahead of the host
launcher (~/.local/bin/ai-hats), silently running stale code. With the
generator gone, no venv producesbin/ai-hats; the bash launcher now execs
<venv>/bin/python -m ai_hats "$@"and probes venv health/usability via
bin/python+ apython -c "import ai_hats"import probe rather than the
removed console-script proxy.is_usable_version/read_current_sha
(paths.py) drop thebin/ai-hatsclause and key on the.completesentinelbin/python(behaviour-equivalent for any real install).python -m ai_hats
routes throughmain_entryso--tree/--help --treeordering is identical
to the old console entry. The host launcher remains namedai-hatsand on
$PATH— only the per-venv generated binary is gone.
v0.9.0
[0.9.0] - 2026-06-17
Added
- Release CI to PyPI via OIDC trusted publishing (HATS-765, child of the
HATS-762 distribution overhaul). A new.github/workflows/release.ymlbuilds
the wheel + sdist withuv buildand publishes to PyPI on av*tag push via
tokenless OIDC trusted publishing — build and publish are split into two jobs
so theid-token: writeprivilege is held only by a publish-only job. This is
the artefact that makes thestablechannel real: end users install a
prebuiltai-hats==<version>wheel instead of a git source build. A
self-skipping live e2e (tests/e2e/test_stable_channel_live.py) exercises a
stable-channelself updateagainst the real PyPI index (skips until the name
is published).docs/RELEASING.mddocuments the trusted-publisher one-time
setup and the post-publish verify step. ai-hats session showrenders a Usage section fromusage.json(HATS-734,
child of HATS-699 / HATS-698 audit) — the HATS-664 producer (compute_usage)
had zero in-src consumers, so a producer regression (the resume-mode discovery
bug fixed below) was invisible for months.session shownow renders a
fail-soft Usage block (measured/static always-on,skill_loads, tool
success-rate, sidechain, parser flags) and listsusage.jsonamong the
session artefacts, making the channel falsifiable.
Changed
- Deleted the dead lifecycle
hooks:composition channel; re-homed the one
real consumer (HATS-707, child of HATS-699 / HATS-698 audit). The
role/traitcomposition.hookschannel (CompositionResult.hooks,
HooksConfig, theLifecycleEventenum,composer._merge_hooks, and
HooksRunner) was composed and displayed inconfig statusbut had zero
runtime execution consumers —HooksRunnerscannedlibrary/hooks/by
filename convention (empty of lifecycle scripts since HATS-314) and never
readresult.hooks;TASK_*events never fired.config statusno longer
advertises a hook subsystem that never runs. The single piece of real intent —
the maintainer'ssession_start: [ai-hats self sync-hooks]git-hook drift net
(HATS-593 layer B), itself silently dead — is re-homed to a direct
WrapRunner._resync_git_hooks()call at session start, for every role
(idempotent, fail-open). Existing user configs with ahooks:block are
unaffected (Compositionisextra="ignore"; no migration).RunSessionEnd
is now the retro-banner-only finalize step. - Claude system prompt no longer carries the
AVAILABLE SKILLSindex
(HATS-701, audit F2 of HATS-698, child of HATS-699 — harness optimization).
ClaudeProvider.build_system_promptappended a skills index built from
SKILL.mdfrontmatter (5,988 chars for the 22-skill maintainer role) while
the same session already passes the composed skills via--plugin-dir
(HITL) / SDK plugin (sub-agent) — Claude Code natively lists every plugin
skill with its full description, so the index was a 2-3x duplicate. It is
now suppressed for Claude (returning ~1.5k tokens to the context window on
every session and every sub-agent spawn, with less selector noise from the
duplicate qualified/unqualified listings) and kept for Gemini, which has no
native skill registry. The two near-identicalbuild_system_promptbodies
are de-duplicated intoProvider._compose_sections(result, *, include_skills);
show-promptnow mirrors the real (index-free) Claude prompt. - Trimmed the always-on
## RULESblock ~2.1 KB (HATS-702, child of
HATS-699 / HATS-698 audit). The block ships verbatim in every composed
prompt for every role and consuming project — the one cost nobody can opt
out of.rule_pause_before_shared_state_write(3,005 → 1,542 chars) drops
the incident-narrative rationale and worked example (→ HYP-026 / HYP-027 /
PROP-052 pointer) while keeping every behavioral clause, the command/
reversibility table, and the hook-backstop + ACK warning — enforcement is
unchanged (pre_bash_shared_state_guard.shis wired unconditionally).
rule_composition_value_contract(1,690 → 1,064 chars) compresses its four
invariants to one-liners + ADR pointer, and its staleproviders.pybudget
comment is corrected (~600→~1.0 KB). Net: block 8,149 → 6,060 chars,
~520 fewer tokens on every session and sub-agent. - Maintainer role injection deduped against its traits (HATS-703, child of
HATS-699 / HATS-698 audit — finding F4). Themaintainerrole injection
re-described its own traits and restated thebrainstorm→…→doneworkflow that
trait-agentalready delivers. Dropped the redundant## Workflow(covered by
trait-agentAgent Protocol +trait-basepessimistic-verification /
concise-communication) and## Delegation(near-verbatimtrait-agent
### Delegation); moved the author-facing "what sets this role apart"
meta-section to a YAML comment so it no longer spends agent prompt budget. The
no-Co-Authored-Bycommit-trailer policy now has a single tracked home — the
ai-hats-maintainertrait — removing the contradictory-precedence risk of the
former 3-way duplication. Role injection is now header + intro +## Guardrails
(~0.5–1 KB/session saved). The three HATS-452 prompt-content e2e tests re-point
their role-own-injection marker from## Workflowto the role intro string. - Skill bodies are no longer eager-loaded on every compose (HATS-706, child
of HATS-699 / HATS-698 audit).Composer.composeread each skill's full
SKILL.mdintoResolvedComponent.injectionfor every session and both
providers, yet the only consumer of a skill's body isai-hats reflect's
role-mirror (_materialize_target_composition) — the GeminiAVAILABLE SKILLSindex reads its own single copy via_extract_frontmatter_description.
The eager read is removed; reflect now reads the body on demand from the
skill'ssource_path. Non-reflect sessions no longer pay oneSKILL.mdread
per skill for a body they never use, and the per-skill double read on Gemini
prompt builds collapses to one. No change to prompt output or reflect
artefacts. (The card's other half — hoisting the identicalbuild_system_prompt
into theProviderbase — was already delivered by HATS-701.)
Removed
ai-hats self cleancommand (HATS-709, child of HATS-699 / HATS-698
audit — finding 2a-F3). A total no-op on v4: framework content is composed
in memory (HATS-294), so the rules/skills mirrors it wiped are empty, the
legacy.agent/{skills,hooks}it swept don't exist, and the.ai-hats-managed
manifest its sweep read was never written (_write_managed_manifesthad zero
callers). The only materialized managed content (library/hooks) is owned by
_refresh. The undocumented command and its dead helper chain
(Assembler._clean/_clean_non_local/_clean_managed_entries/
_write_managed_manifest+ the unreachablepreserve_localbranch and
.library_rulesmarker protocol) are removed (~90 LOC). Re-materialize a
project's managed tree viaai-hats self init/self update.
Migration:docs/migration-v0.9.0.md§4 — the
command was a no-op; drop any calls and useself update/self init.- Write-only
pipeline_metrics.jsontelemetry (HATS-736, child of
HATS-699 / HATS-698 audit — dead-delivery class #5).PipelineHarness.__exit__
wrote a per-runpipeline_metrics.json(terminal zero-output / timeout
incident counters) into a namespace GC'd afterAI_HATS_PIPELINE_KEEP_N
(default 10) runs, with zero readers insrc/— the data expired
unread. Folding it into the sessionmetrics.json(read by
session list --json) was rejected: the harness has no map from its
session_idto a spawned session dir, and the signal is already
observable — per-sessiontimed_outlives in sessionmetrics.jsonand
HarnessReliabilityErroris routed to a meta-PROP by reflect-session. The
writer, its dead imports, and its 5 unit tests are removed;__exit__is
now a no-op (artefacts are still kept and GC'd at the next__enter__).
Fixed
- Two real-subprocess e2e files now run in the pre-push gate that protects
master (HATS-746, audit 4b-F4 of HATS-698).tests/e2e/test_wave1_free_tier.py
(3 free-tier pilots) andtests/e2e/test_wt_merge_ambiguity_guard.py(2 tests —
the HATS-502wt mergeambiguity foot-gun guard) lacked
pytestmark = pytest.mark.integration, so the gate's
-m "(integration or smoke) and not quarantine"selection deselected them;
they survived only by accident in CI Job 1'snot integrationpool. Adding the
marker pulls all 5 into the gate (deliberate coverage increase) — a regression
in the foot-gun guard no longer ships to master silently. Also deleted the
deadexternal_envpytest marker (declared inpyproject.toml, zero uses
repo-wide), and recorded on HATS-695 that the two quarantinedself update
pip tests have zero automated coverage (gate deselects viaquarantine, CI
Job 1 viaintegration, CI Job 2 via--ignore=tests/e2e/) until that task
de-flakes and un-quarantines them. - Pipeline engine raises a typed
StepErrorfor a required ctx key absent at
runtime, instead of a bareKeyError(HATS-739, audit 2c-F8 of HATS-698).
_run_stepsprojectedrequires(kwargs = {k: state[k] for k in s.io.requires})
before the per-steptry, so when a producer legally omitted a declared
produceskey at runtime (None-filtered merge;ComposeRoleemits{}for no
role — ADR-0005 value contract), the missing-key lookup raised a context-free
KeyErrorthat bypassed bothfailure_policy="continue"and the_emittrace
event. The projection is now non-raising and an explicit presence check raises a
StepErrornaming the step + missing keys inside thetry, so trace and
continue-policy semantics apply. Latent (no shipped pipeline...
v0.8.0
[0.8.0] - 2026-06-07
Added
compute_usagestep +usage.jsonper-session context-cost report
(HATS-664, first child of HATS-663 session-observability epic) — a transcript-
first parser that turns one Claude Code JSONL session into a machine-readable
usage/v1report: measured always-on budget (firstcache_creationproxy), an
ordered event timeline (skill-body loads viaSkilltool_use, reference Reads
of*/references/*.md+SKILL.md, tool calls withis_error, stop-hook
firings), aggregates with tool success-rate, and sub-agent sidechain linkage
(detect + link bysessionId/sourceToolAssistantUUID, no per-event token
merge). The report also self-describes its ai-hats context —role/
provider/exit_codecopied from the session'smetrics.json(so the
comparison sibling pairs sessions by role and "what went wrong" debugging reads
it in one place); whenroleresolves, a staticcosts.pyper-component
always-on breakdown is attached underalways_on.staticfor a measured-vs-
static cross-check. The pureparse_session_usage(src/ai_hats/usage.py) is
transcript-only and fail-soft (malformed line / unknown entry type →flags,
never a crash — verified over all ~550 historical transcripts with zero
crashes) and
doubles as a bash-composable primitive (python -m ai_hats.usage <jsonl>,
JSON to stdout) for retroactive sweeps. TheComputeUsagestep is the thin
live driver — sibling ofmake_audit, same post-session JSONL, wired right
after it in bothfinalize-hitlandfinalize-subagent— so every new
session writes<session_dir>/usage.jsonalongsideaudit.md/metrics.json.
Reproduces the HATS-578 finding automatically (skill-BODY loads are rare —
~20% of sessions;backlog-manager+self-retrospectivedominate). Per-event
token attribution is a documentedreconstructedheuristic (per-message usage
is a per-turn total); unattributable events keeptokens_delta = null, never a
magic0(honorsrule_composition_value_contract §3).devils-advocateskill + conditional "Approach & counter" plan section
(HATS-621, M3 of HATS-629) — the value-counter stage of the plan-gate. A new
required=FalseApproach & countersection sits betweenRequirementsand
Scope & Out-of-scope(PLAN_SECTIONS); the engine never blocksexecuteon
it (the "non-trivial plans fill it or write explicitN/A" norm is behavioural,
carried by the skill + companion HYP). Thedevils-advocateskill ships the
4-step skeptic method — steelman the value → name the unstated assumption →
counter it (needed? missed anything? another way?) → assess impact — and is
wired intotrait-agent.plan-gatedocuments the
requirements-interview ⇄ devils-advocate → design-minimalismflow, with
cross-refs in both sibling stages. Catches "right scope, wrong direction" — the
failure mode neitherrequirements-interview(WHAT) nordesign-minimalism
(HOW MUCH) catches.plan-disciplineskill (HATS-643) — the named discipline for the plan-home
invariant: a plan is always a task, authored directly into the canonical
<ai_hats_dir>/tracker/backlog/tasks/<ID>/plan.md, and never routed through
.claude/plans(inert plan-mode scratch ≠ the plan). Carries the draft→tracker
transfer procedure and hands off toplan-gatefor section filling; the engine
per-section gate (HATS-635) remains the enforcement backstop. Wired into
trait-agent. Closes the plan-mode→.claude/planssalvage loophole left after
HATS-637 at the discipline layer.backlog-managerandrule_backlog_discipline
now point here instead of duplicating the flow. Covers the Claude Code plan-mode
two-phase reality (HATS-644): plan mode is read-only, so the.claude/plans
draft is expected Phase-1 scratch and the mandatory first post-approval action is
to transfer it into the trackerplan.md; when plan mode isn't forced, plan
directly in the tracker.ai-hats task hyp create --verification-protocol TEXT(HATS-623).
library-change-hypothesis-protocolmandates averification_protocol
field on companion HYPs, buthyp createexposed no flag and
rule_backlog_disciplineforbids editinghypotheses/*.yamldirectly —
so HATS-616 had to fold the protocol text into--success-criterion. The
Hypothesismodel is alreadyextra="allow"and persists via
model_dump(exclude_none=True), so the field round-trips with no model or
storage change; the flag is dropped from the YAML when omitted. Consumed
byreflect/session-reviewer handoff (HATS-534).dev-webrole — web/frontend development (JS/TS + React) (HATS-616).
Fills the one real library gap from the awesome-claude-skills review (no
web role; Go had 40+ skills). Shape mirrorsdev-python+dev::python:
a single roledev-webover one geardev::web(not ago-dev-style
multi-gear split). The gear carries JS/TS + React + a11y + tooling
conventions and bundles two seed skills:ui-ux-review(two-mode —
guide + P0/P1/P2 review — cognitive UX rules, distilled from
oil-oil/oiloil-ui-ux-guide [Apache-2.0] and wondelai/skills [MIT]) and
webapp-testing(Playwright recon→act→assert browser verification,
distilled from anthropics/skills [Apache-2.0]).task_completegates:
npm run lint/test/build+npx playwright test. Companion HYP-056
tracks the expected behavior shift. Per-source licenses + attribution
recorded in each skill'smetadata.yamlupstream:block.- Skills can declare provider runtime hooks (
runtime_hooks:in a
skill'smetadata.yaml, HATS-597 / HATS-601). Mirrors thegit_hooks
open registry: a composed skill declares hooks keyed by Claude event
(v1:PreToolUse,PostToolUse), each row{matcher, script}. On
self init/self updatethe assembler materializes each declared
script to<ai_hats_dir>/library/hooks/<skill>-<basename>.sh(0o755,
manifest-tracked, swept when the skill leaves the role) and
ClaudeProviderwires one managed.claude/settings.jsonentry per
(event, skill, matcher), taggedai-hats:<skill>:<event>:<matcher>.
A hook whose script cannot be resolved is skipped on both sides, so
settings.json never points at a missing file. User-authored hook
entries are never touched; Gemini is a no-op. The hard-coded HATS-437
shared-state guard path is unchanged (its migration onto the registry
is HATS-598). - Migration safety chain — backup-first + smoke-assert + user-hooks
namespace (HATS-549). Hardensai-hats self update/
non-greenfieldself initagainst data-loss regressions of the
class that produced the proxmox failure mode (user-authored
.agent/hooks/pre_bash_secret_guard.pysilently deleted by an
older bump codepath, healer auto-rewriting the orphan ref in
.claude/settings.json, every Bash tool call thereafter printing
/bin/sh: <path>: No such file or directory). Four phases:- Phase 1 — pre-bump snapshot (
src/ai_hats/migration_backup.py).
Before any destructive step runs, snapshots the ai-hats-managed
surface (.agent/,.claude/settings*.json,ai-hats.yaml,
CLAUDE.md/GEMINI.md,.githooks/,.gitignore) to
/tmp/ai-hats/bump-backups/<utc-ts>-<slug>-<label>.tar.gz.
Path printed to stderr withRecovery: tar -xzf <path> -C <project>one-liner BEFORE any work starts. Retention sweep
keeps last 10 per project-slug. Excludes.venv/
__pycache__/.cache/node_modules/*.pyc/ symlinks
(regenerable / safety risks). Hard-fail on
BackupError: proceeding without a snapshot defeats the
safety guarantee. Env knobs:AI_HATS_BUMP_BACKUP_DIR=<path>
overrides base dir;AI_HATS_BUMP_BACKUP_DIR=-hard-disables
(one stderr WARN per call, for CI / sandbox). - Phase 4 —
user-hooks/namespace + disable-vs-rewrite
(paths.user_hooks_dir,
Assembler._migrate_layout_v4_hooks_partition,
migration_healer._disable_user_hooks_in_settings).
Project-authored files under legacy.agent/hooks/(anything
whose basename is NOT in_ai_hats_owned_hook_basenames())
relocate to<ai_hats_dir>/user-hooks/— disjoint from the
managedlibrary/hooks/namespace. The matching
.claude/settings.jsonPreToolUse entry is REMOVED (not
auto-rewritten); Stage B inventory carries a copy-paste JSON
re-enable snippet. A second reconciliation pass walks
library/hooks/for foreign content that landed there via a
pre-HATS-549 auto-heal and relocates it touser-hooks/—
next bump heals stuck states inherited from prior versions
transparently.
- Phase 1 — pre-bump snapshot (
- Install diagnostics in
ai-hats config statusHealth section
(HATS-497).config statusnow prints install-level fields
alongside the existing project-side health checks:Version,
Interpreter(Python executable + version),Venv,Source
(editable / pinned / git, with ref and short SHA where applicable),
Librarypath,Resolved via(heuristic overAI_HATS_VENVenv >
ai-hats.yamlvenv_path> default), andRepo HEAD(editable
installs only — short SHA + branch + clean/dirty). Pip-managed
direct_url.json(PEP 610) is the source of truth forSource;
HATS-496's--revisionwrites the ref that lights up the "pinned @"
display. Refactor: the Health block now prints regardless of whether
a role is active — install info is useful before init too (e.g.
troubleshooting "what version am I on, where does it live" on a
fresh checkout). - Docs: dev-vs-runtime venv discipline in CONTRIBUTING.md
(HATS-494). New### Stable runtime vs editable dev install
subsection under## Development setupcodifies the
two-venv pattern (AI_HATS_VENVenv override +ai-hats self update --revision <REF>to pin the stable venv to a known-good tag), with
caveats about editable installs (frozen `pyproject.toml...
v0.7.0
[0.7.0] - 2026-05-23
Composition-and-customization release. MAJOR bump driven by three shifts:
- v0.6 → v0.7 layout migration is now folded into
self update/
self bump; the standaloneself migrate-v07verb is retired
(Migration:under Removed). - User-level overlays at
~/.ai-hats/customizations.yamlship as a
first-class layer;personal-workflowmigrates there
(Migration:in the ✨ BREAKING section). - Role architecture splits —
assistant= opinionated default
(Google Workspace + personal-workflow bundled);dev-python= clean
Python baseline;maintainer= new role for ai-hats-codebase work.
Also: composition is now an immutable contract (ADR-0005, HATS-452),
two-level defence against autonomous shared-state writes (HATS-437),
banner reads real git state (HATS-432) + fires on non-editable installs
(HATS-458), self update refuses silent downgrades (HATS-441) and
short-circuits pip on a no-op, wt merge has a pre-merge drift guard
(HATS-457).
🎭 v0.7 role architecture — maintainer + dev-python extraction (HATS-381 + HATS-392)
maintainer extracted (HATS-381). Codebase work on ai-hats itself
moves out of assistant into a dedicated role. New shipped content:
core/skills/design-minimalism— every primitive at plan stage needs
a concrete use case; speculative additions → Out of scope.core/skills/predictive-accounting— for shrink/refactor tasks,
present baseline + delta before implementation.usage/skills/doc-protocol— plan-stage style forks + scope triage- pre-commit artifact verification (folds three prior memory-only
patterns).
- pre-commit artifact verification (folds three prior memory-only
core/rules/rule_core_vs_usage_split— universal-vs-project-specific
decision tree for library content (sourced from PROP-037).core/traits/ai-hats-framework— wraps the rule + layered-library
injection.
The ai-hats-maintainer trait injection grew from ~10 to ~90 lines:
Conventional Commits, what-NOT-to-commit, canonical CLI, glossary-first,
numbered-refs, d2 practical gotchas, release flow, 8 architectural
defaults, 3 anti-patterns. Replaces the last per-project memory
references.
dev-python extracted (HATS-392). assistant (8 traits) is
reframed as opinionated all-in-one — bundled Google Workspace +
personal-workflow; not a clean baseline. New dev-python (6 traits)
is the clean Python + Shell starter. Wizard Step 3 maps pyproject.toml
/ setup.py → dev-python; empty / non-Python projects still →
assistant.
✨ Bring your own traits/skills — user-level overlays (HATS-421 + HATS-433, BREAKING)
The mechanism (HATS-421). A second customization layer lives at
~/.ai-hats/customizations.yaml — same schema as project-level,
applied to every project. No more repeating ai-hats config customize
across N projects; personal content no longer leaks into the package.
mkdir -p ~/.ai-hats/traits/<your-trait>
$EDITOR ~/.ai-hats/traits/<your-trait>/config.yaml
ai-hats config customize <role> --add-trait <your-trait> --global
ai-hats config status # full tree with (built-in) / (global) / (project) source-tagsCompose order: built-in → global → project (project wins on conflict).
config status annotates every component with a source-tag.
Migration: HATS-433, BREAKING. personal-workflow trait —
TEMPORARY in v0.6 — leaves the package and moves to user-scope. Affects
maintainer (10 → 9 traits) and assistant (8 → 7 traits). Trait body
unchanged.
mkdir -p ~/.ai-hats/traits/personal-workflow
# Recover content from the previous tag, then:
ai-hats config customize maintainer --add-trait personal-workflow --global
ai-hats config customize assistant --add-trait personal-workflow --global
# In each project:
ai-hats self bumpWorked example: docs/how-to-extend.md §"Migrating from a removed
built-in component".
Added
- HATS-445 —
ai-hats execute --prompt <name>resolves
initial_injections/<name>.mdthrough the fulllibrary_pathschain.
Unlocks shell-alias custom verbs: plugin authors ship a role +
injection and wrapai-hats executein a shell function — custom verb
with zero ai-hats core changes. New section in
docs/how-to-extend.md: "Custom verbs via shell aliases". - HATS-444 —
docs/INDEX.mdis the single source of truth for the
wizard's companion-docs catalog. Mechanical enforcement via new git
pre-commit hook (pre-commit-docs-index.sh) blocks commits that
stage structural docs/ changes without stagingINDEX.md. Override:
AI_HATS_DOCS_INDEX_ACK=1. - HATS-437 — Two-level defence against autonomous shared-state
writes (HYP-026 + HYP-027). Always-on rule
rule_pause_before_shared_state_writeforbidsgh pr create/close/merge,gh issue comment,gh release create,
git push,TaskCreatewithout per-command pause + user confirmation,
and bans chaining them in one Bash invocation. Two hook scripts back
the rule with deterministic blocks on the irreversible subset
(gh pr merge,git push --force). Per-command ack via
AI_HATS_SHARED_STATE_ACK=1. Gemini sessions get the rule +
pre-push hook only (no PreToolUse equivalent in Gemini CLI). - HATS-442 — Session audit records the effective role composition
snapshot (traits + rules + skills with source-tags) at session
start.session-reviewercites source-tags when filing proposals
(framework vs user vs project). Closes the observability gap created
by HATS-421. - HATS-408 —
ai-hats self migrate-v07one-shot safe migration from
v0.6 to v0.7. Inspects on-disk artefacts, diffs each vs composition
baseline, refuses on user edits (--forcebypasses). Atomic single
git commit; idempotent. (Superseded by HATS-415 — see Removed.) - HATS-401 — Session-end Update banner in
execute/human
pipelines. When installed SHA lags upstream, surfaces short SHAs +
ai-hats self updatehint under✨ Session summary. Non-blocking
detached probe writes to<ai_hats_dir>/.cache/update-check.json
(24h TTL). Opt-out:AI_HATS_NO_UPDATE_CHECK=1.
Changed
- HATS-415 —
ai-hats self updateandself bumpself-heal
v0.6 → v0.7 layouts inline. Safe-to-delete v0.6 files (bytes match
baseline) are swept transparently; user-edited files raise
AssemblyErrorwith per-file guidance. New flags:--migrate-force
(bypass refusal) and--check-branches(warn on local branches
modifying paths slated for deletion). No auto-commit — user owns the
commit decision. - HATS-294 — Composition is now per-session in memory; canonical
layer no longer materialisespriorities.md/role.md/
traits/*.md/rules/*.md/skills_index.md.write_canonical
emits only theimports.mdaggregator. Providers'build_override
renamed tobuild_session_prompt. - Migration: HATS-407 —
ai-hats role set <name>is yaml-only
(writesdefault_role:toai-hats.yaml). Removed
ai-hats self rollback— yaml-only config meansgit checkout ai-hats.yamlis the recovery path. Users scriptingself rollback
should switch togit checkout.
Removed
- Migration: HATS-415 —
ai-hats self migrate-v07CLI command
removed. Its logic lives inline inAssembler.bump()and surfaces on
self update/self bump. Flags re-homed:--force→
--migrate-force,--check-brancheskept.--no-commithas no
analog. Migration: drop theself migrate-v07invocation, run
ai-hats self update— sweep auto-applies on a v0.6-shape project.
Fixed
.gitignorelegacy block sweep —ai-hats self bump/self updatenow removes the pre-HATS-317# AI-HATS:START..ENDmanaged
block from user.gitignorefiles. HATS-317 retired the dynamic
generator in favour of a single static line at init, but never
shipped the one-shot cleanup — every project initialized before
HATS-317 carried 50–90 stale per-component entries
(.agent/ai-hats/rules/X.md,traits/Y.md, etc.), many pointing at
v0.7-vanished paths after HATS-294 stopped materialising the
canonical layer. Doubly stale: redundant (the bare.agent/
user-init line covers the subtree) AND broken (paths no longer
exist). NewAssembler._strip_legacy_managed_block()strips the
block + one preceding blank-line separator, idempotent, respects
manage_gitignore = False. Delivery pattern matches HATS-413:
persisted onself bumponly, no rewrite-on-read. Dogfooded on
ai-hats's own.gitignore(121 → 48 lines).ai-hats self update— short-circuitpip installwhen installed
SHA already matches remotemaster. Saved 10-15 s per no-op update
(60s+ on slow links — users mistook for hang). Reuses the
HATS-432/441 ahead/behind probe; bump still runs in-process so
migrations apply. Bump path gained a Rich spinner so the
heal_external_refswalk no longer looks like a hang.- HATS-457 —
ai-hats wt mergedrift guard (HYP-017). Between
wt createandwt mergethe base branch could advance — another
agent's merge into localmaster, ororigin/<base>pulled in
commits — and the pre-mergegrep-verifybecame silently stale.
WorktreeManager.createsnapshots base SHA;wt mergedoes a
best-effortgit fetchand refuses withWorktreeDriftErroron
divergence. New--accept-driftflag (separate from--force—
two checks, two flags). Legacy state files gracefully skip. - HATS-452 — composition / pipeline value contract. Bare
ai-hats
was writing aprompt.mdmissing the merged role/trait injection —
16k chars of behavioral guidance never reached the agent. Root cause:
compose_rolereturned{"system_prompt": ""}for missing role;
WrapRunner.run_sessionaccepted the empty string and replaced the
freshly-composed list with[""]. Four-layer fix per
ADR-0005:
immutableCompositionResult, funnel dropsNoneat merge bo...