Skip to content

Releases: muratovv/ai-hats

v0.15.0

Choose a tag to compare

@muratovv muratovv released this 31 Aug 11:37

[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_gate is a symlink to the real hooks/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, so ai_hats_library.hooks.consent_gate was absent from every published wheel, 0.5.7 included. Nothing noticed while no installed code imported it; ai_hats.consent_wrapper is new in this release and imports it at module level, so a fresh pip install would have raised ModuleNotFoundError on the first HITL session that composed a role declaring apps.consent_gate. Measured against the real artefact rather than the build: the wheel downloaded from PyPI carries six consent_gate files 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 in docs/ at all. docs/reference-env.md now 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 way tests/e2e/CATALOG.md already 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 where ai_hats is 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=abc raised ValueError and 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, _OFF or _SKIP suffix. AI_HATS_SKIP_SELF_LOCATION_GUARD carries 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 blank AI_HATS_SKIP_RETIRED_PRUNE, handing a child the uv pip uninstall its parent had suppressed. Only the guard hatch is an approval, so it is named explicitly beside AI_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 channel and 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.surfaces and ai_hats.surfaces.<surface> reached the schema layer through the surface contract, and ai_hats_core cost pydantic, filelock and asyncio to reach Deadline, 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, moving Deadline out 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 way ai_hats_observe already did; every exported name resolves as before, the deprecated Provider aliases included. One shape does change: a bare import ai_hats_core no longer exposes ai_hats_core.locks and its six sibling submodules as attributes, because nothing imports them for it any more. from ai_hats_core import locks still works, and the package's documented API — __all__ plus safe_delete — is untouched. Because one eager import anywhere on that path silently restores the whole cost, tests/test_dispatcher_import_closure.py refuses 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 naming AI_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_ACK appeared 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 at githooks_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 each Notification, several per turn, put safety_gate.py, pre_bash_shared_state_guard.sh and backlog_write_gate.py through a call that does not exist. The same collapse keyed the user's own channel wrong, so their Stop, Notification and PostInvocation hooks stopped running entirely while their PreToolUse hooks 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. SurfaceProfile had 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_journal writes 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 reads file_path or path and ALLOWS the call when neither is present (wt_gate.py). The surface's profile declared no argument renames at all, so read, edit and write reached the gates with a key none of them look at. The name is observed rather than guessed — opencode's own session database records filePath on all 154 reads, edits and writes it holds, while grep and glob send path, which the gates already read — the same kind of evidence that produced codex's exec.

  • Codex ran no terminal gate at all (HATS-1858). Codex names its shell exec; the tool-name table in src/ai_hats/surfaces/codex/claude_hook_adapter.py had no termi...

Read more

v0.14.0

Choose a tag to compare

@muratovv muratovv released this 29 Jul 22:11

[0.14.0] - 2026-07-29

Removed

  • The legacy ai-hats task CLI is unmounted (HATS-1260, epic HATS-1159).
    The four command groups — task, task hyp, task proposal, task attach
    (28 verbs) — are gone from the ai-hats surface; rack is 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), so ai-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_prefix from ai-hats.yaml only — legacy projects should set it
    explicitly.

  • The backlog-manager skill tree and its git hook are deleted (HATS-1261,
    epic HATS-1252). Once hatrack became 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 the pre-commit-attachments.sh hook
    that guarded the legacy card subtree are gone from ai-hats-library;
    hatrack is the only shipped backlog-manager skill. Migration:
    docs/migration-v0.14.0.md.

  • packages/ai-hats-tracker is deleted (HATS-1262, epic HATS-1252). The
    package backing the retired ai-hats task CLI 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_context and
    TrackerPaths into src/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; see self update under Fixed.

Fixed

  • A no-op self-heal no longer deadlocks the CLI in an infinite re-exec loop
    (HATS-1359). bootstrap_or_die() treated attempt_self_heal()'s exit code as
    proof the missing dependency was importable, then os.execv'd
    unconditionally. But uv 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. Because cli.main() calls it before subcommand
    dispatch, even ai-hats self update — the in-band fix — hung, leaving no
    escape but an out-of-band uv command. 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
    : deleting ai-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 update prunes distributions the new version retired
    (HATS-1280). self update installs, it does not synchronize: a dependency the
    new version dropped stayed in the venv with its console scripts. After 0.14.0
    that would leave ai-hats-tracker installed 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 when uv is unavailable.
    Migration: docs/migration-v0.14.0.md.

  • wt merge refuses 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 → done consumed 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, one git gc
    from gone. merge now resolves the task branch tip at merge time; a
    caller-supplied expected_tip that no longer matches is a typed
    WorktreeStaleRefError naming 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
    WorktreeMergeIncompleteError and 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 the review → execute rework
    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 the git-mastery pre-commit smoke hook
    resolved pytest through PATH — landing on the main checkout's interpreter,
    which the wrong-checkout guard then refused. The very first git commit inside
    a new worktree failed, and the printed remedy was a ten-line manual
    provisioning recipe. A new worktree-venv skill contributes a wt_in hook
    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-mastery skill, and it passed tests/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 an integration-
    tagged task in execute and no such directory had every commit blocked.
    The path is now passed only when it exists; without it pytest falls back to the
    configured testpaths, which is the pre-scoping behaviour.

  • ai-hats-agy is published (HATS-1353). The agy surface was listed in
    KNOWN_SURFACES and self-heal ran uv pip install ai-hats-agy for 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 from release-packages.yml in its own pypi-agy environment, and a
    test pins that every surface in the registry has a publish job.

  • The ai-hats binary runs from inside a worktree (HATS-1306). The launcher
    resolved the project venv relative to cwd, so any invocation inside a linked
    worktree died with venv missing at <worktree>/.agent/ai-hats/.venv. That
    killed ai-hats wt exec <branch> -- git commit outright 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
    reported Task not found, --set could not repair it because it validates
    before it mutates, and the only way out was hand-editing task.yaml. Reads are
    now tolerant and writes strict: from_yaml coerces stray entries and reports
    them as warnings, save refuses 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 and epicify ever reached
    audit.jsonl — there were zero records for --link, --unlink, --set,
    --append, --log or the document ops. A re-parent left no trace at all, and
    the mirrored side learned nothing. transition now emits op:* records
    carrying the field or document name, and the post-lock mirror delta is
    persisted on the target card rather than dropped.

  • A second fold is 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 typed already_folded refusal naming the
    current target, and folded_into declares the derived inverse subsumes, so
    "what was folded into this card" is answerable again.

  • The documented re-parent command is the one that works (HATS-1350). The
    hatrack skill and docs/how-to-hatrack.md both taught
    rack transition <ID> --set parent_task=<EPIC>, which rack refuses, and the
    refusal's own suggestion then failed already_linked because 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 at wt create, so a branch
    rebased onto the moved base was still refused with "N commits ahead" — and the
    only override, --accept-drift, lives on ai-hats wt merge, which
    rack transition cannot 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-drift to what it always meant: mergin...

Read more

v0.13.2

Choose a tag to compare

@muratovv muratovv released this 09 Jul 22:55

[0.13.2] - 2026-07-10

Added

  • Provider open-registry + entry-points IoC seam (HATS-870, T10). The closed
    PROVIDERS dict is now an open registry: built-ins self-register at import and
    third parties register via register_provider() or the ai_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 the cline CLI as a provider via
    the ai_hats.providers entry 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
    -s role delivery, interactive TUI for HITL, headless --yolo --json for the
    automate path. A transcript parser (ClineParser) and native .cline/skills/
    materialization landed as follow-ups — see the ai-hats-cline changelog.

Fixed

  • Unknown --provider fails 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 uncaught ValueError.

  • Worktree-isolation gate no longer fires on unrelated repos (HATS-959). The
    wt_gate.py PreToolUse 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 payload cwd'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 unresolvable cwd (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 the ai_hats_wt public surface
    (get_default_base_branch, get_default_merge_branch) and edited
    locks.py / manager.py after 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.py was 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-managed marker) 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/skills is
    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

Choose a tag to compare

@muratovv muratovv released this 06 Jul 20:21

[0.13.1] - 2026-07-06

Added

  • Worktree lifecycle effects recorded in the task card (HATS-866).
    ai-hats wt create / merge / discard now append a structured effect line
    to the card's work_log/ (branch, worktree path, merge SHA), so the tracker
    carries the worktree history. Routed through a WorktreeEffects seam that
    decouples state from wt.
  • Owner registry + unclaimed-marker sweeper (HATS-905 phase 1, HATS-910).
    Every mechanism materializing files outside <ai_hats_dir> registers an
    owner_key in the open registry (ai_hats.owners); on self init/bump a
    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-publish owners), and the publish
    manifest path gained the HATS-907 traversal guard.
  • Hashed owner_key marker convention (HATS-905 phase 2, HATS-911).
    Line-manifest markers are written via ai_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-manifest now 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.yaml writers no longer lose
    each other's changes
    (HATS-526). Every config writer (customize, config set, init/bump, session-start set_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 --global kept 1 of 3). Writes now go through
    locked_update: a cross-process file_lock (new ai_hats_core primitive,
    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 exec and the worktree env thread packages/*/src (ai_hats_core,
    ai_hats_wt) into PYTHONPATH, 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-hats can no longer resolve stale workspace
    subpackages
    (HATS-923, HATS-928). ai-hats-core published at 0.3.0 (adds the
    file_lock / LockTimeoutError RMW-lock helper) and ai-hats-wt at 0.2.1
    (adds WorktreeHook / 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-pins ai-hats-core>=0.3.0 and
    ai-hats-wt>=0.2.1.

v0.12.0

Choose a tag to compare

@muratovv muratovv released this 02 Jul 20:34

[0.12.0] - 2026-07-02

Added

  • Standalone ai-hats-core + ai-hats-wt packages (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-hats now depends
    on them (ai-hats-core>=0.1.0, ai-hats-wt>=0.1.0) and ai-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 PreToolUse guard (HATS-632). The tool-call-hygiene
    skill now ships a non-blocking PreToolUse Bash runtime hook: when a command
    is a pure invocation of grep/find/cat/sed -i/… that a dedicated tool
    covers, it injects an additionalContext nudge 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-library runtime_hooks
    consumer, setting the shared stdin tool_input → JSON hookSpecificOutput
    convention for the behavior-hook family.
  • Python security-lint PostToolUse hook (HATS-660). A new py-security-lint
    skill (composed by the dev::python trait) runs ruff check --isolated --select S
    (flake8-bandit security rules) on every .py you Edit/Write and forwards any
    findings to the agent via a non-blocking additionalContext note — an early,
    edit-time security layer that complements (does not replace) the project's CI
    lint. Zero egress, fail-open when ruff is absent. Kill switch:
    AI_HATS_SECURITY_LINT_OFF=1.

v0.11.0 — worktree lifecycle & merge robustness

Choose a tag to compare

@muratovv muratovv released this 26 Jun 14:03

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 done tolerates an already-merged, state-lost branch
    (HATS-697). When work shipped on the base out-of-band (manual git merge --no-ff task/<id>) and/or the auto-worktree was removed by hand, done
    refused with a false worktree 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 execute spins no fresh worktree (HATS-697). transition execute --force is 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 / close is 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 (false task 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 done raise an "incomplete worktree state" error naming the
    field instead of an opaque TypeError.
  • execute --batch without --role fails cleanly (HATS-827), instead of
    crashing on an invalid agent//<session> worktree branch.

Added

  • Capstone e2e matrix test_worktree_lifecycle_robustness_matrix.py asserting
    the epic's invariants hold together on the real launcher + binary.

v0.10.0

Choose a tag to compare

@muratovv muratovv released this 20 Jun 13:59

[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_location into main_entry, refuses-and-instructs
    on a foreign invocation — prints remediation_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/--tree info command resolves to
    sanctioned), is wired into main_entry (not the main click group, so
    in-process CliRunner tests 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) and ai_hats.cli.maintenance
    (find_stray_launchers) scan $PATH for stray ai-hats binaries outside the
    sanctioned launcher and WARN — never delete.
  • Forward-safe ai-hats.yaml reader — preserve unknowns, fail loud on a newer
    schema
    (HATS-792, child of HATS-786). ProjectConfig now round-trips a
    same-version unknown top-level field instead of dropping it: from_yaml
    stashes the pre-stripped unknown keys on an _extra PrivateAttr and
    to_dict merges them back (mirrors TaskCard.extras), so an OLDER ai-hats
    preserves (does not silently delete on save()) a field a NEWER ai-hats wrote
    without a schema_version bump — while the HATS-581 stderr WARN still fires.
    A genuinely newer schema fails loud: from_yaml raises ProjectConfigError
    pointing at ai-hats self update when on-disk schema_version exceeds
    KNOWN_SCHEMA_VERSION (4), and a matching save() 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.md for
    the one-time crossover (reinstall the host launcher; clear stray app-venv
    installs). Removed the ai-hats console-script entry point; python -m ai_hats is now the sole package entry (HATS-790, Alt 5). The [project.scripts] ai-hats = "ai_hats.cli:main_entry" generator made every venv depending on ai-hats
    materialise a bin/ai-hats that direnv could prepend ahead of the host
    launcher (~/.local/bin/ai-hats), silently running stale code. With the
    generator gone, no venv produces bin/ai-hats; the bash launcher now execs
    <venv>/bin/python -m ai_hats "$@" and probes venv health/usability via
    bin/python + a python -c "import ai_hats" import probe rather than the
    removed console-script proxy. is_usable_version / read_current_sha
    (paths.py) drop the bin/ai-hats clause and key on the .complete sentinel
    • bin/python (behaviour-equivalent for any real install). python -m ai_hats
      routes through main_entry so --tree / --help --tree ordering is identical
      to the old console entry. The host launcher remains named ai-hats and on
      $PATH — only the per-venv generated binary is gone.

v0.9.0

Choose a tag to compare

@muratovv muratovv released this 19 Jun 13:20

[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.yml builds
    the wheel + sdist with uv build and publishes to PyPI on a v* tag push via
    tokenless OIDC trusted publishing — build and publish are split into two jobs
    so the id-token: write privilege is held only by a publish-only job. This is
    the artefact that makes the stable channel real: end users install a
    prebuilt ai-hats==<version> wheel instead of a git source build. A
    self-skipping live e2e (tests/e2e/test_stable_channel_live.py) exercises a
    stable-channel self update against the real PyPI index (skips until the name
    is published). docs/RELEASING.md documents the trusted-publisher one-time
    setup and the post-publish verify step.
  • ai-hats session show renders a Usage section from usage.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 show now renders a
    fail-soft Usage block (measured/static always-on, skill_loads, tool
    success-rate, sidechain, parser flags) and lists usage.json among 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/trait composition.hooks channel (CompositionResult.hooks,
    HooksConfig, the LifecycleEvent enum, composer._merge_hooks, and
    HooksRunner) was composed and displayed in config status but had zero
    runtime execution consumers — HooksRunner scanned library/hooks/ by
    filename convention (empty of lifecycle scripts since HATS-314) and never
    read result.hooks; TASK_* events never fired. config status no longer
    advertises a hook subsystem that never runs. The single piece of real intent —
    the maintainer's session_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 a hooks: block are
    unaffected (Composition is extra="ignore"; no migration). RunSessionEnd
    is now the retro-banner-only finalize step.
  • Claude system prompt no longer carries the AVAILABLE SKILLS index
    (HATS-701, audit F2 of HATS-698, child of HATS-699 — harness optimization).
    ClaudeProvider.build_system_prompt appended a skills index built from
    SKILL.md frontmatter (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-identical build_system_prompt bodies
    are de-duplicated into Provider._compose_sections(result, *, include_skills);
    show-prompt now mirrors the real (index-free) Claude prompt.
  • Trimmed the always-on ## RULES block ~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.sh is wired unconditionally).
    rule_composition_value_contract (1,690 → 1,064 chars) compresses its four
    invariants to one-liners + ADR pointer, and its stale providers.py budget
    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). The maintainer role injection
    re-described its own traits and restated the brainstorm→…→done workflow that
    trait-agent already delivers. Dropped the redundant ## Workflow (covered by
    trait-agent Agent Protocol + trait-base pessimistic-verification /
    concise-communication) and ## Delegation (near-verbatim trait-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-By commit-trailer policy now has a single tracked home — the
    ai-hats-maintainer trait — 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 ## Workflow to the role intro string.
  • Skill bodies are no longer eager-loaded on every compose (HATS-706, child
    of HATS-699 / HATS-698 audit). Composer.compose read each skill's full
    SKILL.md into ResolvedComponent.injection for every session and both
    providers, yet the only consumer of a skill's body is ai-hats reflect's
    role-mirror (_materialize_target_composition) — the Gemini AVAILABLE SKILLS index reads its own single copy via _extract_frontmatter_description.
    The eager read is removed; reflect now reads the body on demand from the
    skill's source_path. Non-reflect sessions no longer pay one SKILL.md read
    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 identical build_system_prompt
    into the Provider base — was already delivered by HATS-701.)

Removed

  • ai-hats self clean command (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_manifest had 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 unreachable preserve_local branch and
    .library_rules marker protocol) are removed (~90 LOC). Re-materialize a
    project's managed tree via ai-hats self init / self update.
    Migration: docs/migration-v0.9.0.md §4 — the
    command was a no-op; drop any calls and use self update / self init.
  • Write-only pipeline_metrics.json telemetry (HATS-736, child of
    HATS-699 / HATS-698 audit — dead-delivery class #5). PipelineHarness.__exit__
    wrote a per-run pipeline_metrics.json (terminal zero-output / timeout
    incident counters) into a namespace GC'd after AI_HATS_PIPELINE_KEEP_N
    (default 10) runs, with zero readers in src/ — the data expired
    unread. Folding it into the session metrics.json (read by
    session list --json) was rejected: the harness has no map from its
    session_id to a spawned session dir, and the signal is already
    observable — per-session timed_out lives in session metrics.json and
    HarnessReliabilityError is 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) and tests/e2e/test_wt_merge_ambiguity_guard.py (2 tests —
    the HATS-502 wt merge ambiguity 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's not integration pool. 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
    dead external_env pytest marker (declared in pyproject.toml, zero uses
    repo-wide), and recorded on HATS-695 that the two quarantined self update
    pip tests have zero automated coverage (gate deselects via quarantine, CI
    Job 1 via integration, CI Job 2 via --ignore=tests/e2e/) until that task
    de-flakes and un-quarantines them.
  • Pipeline engine raises a typed StepError for a required ctx key absent at
    runtime, instead of a bare KeyError
    (HATS-739, audit 2c-F8 of HATS-698).
    _run_steps projected requires (kwargs = {k: state[k] for k in s.io.requires})
    before the per-step try, so when a producer legally omitted a declared
    produces key at runtime (None-filtered merge; ComposeRole emits {} for no
    role — ADR-0005 value contract), the missing-key lookup raised a context-free
    KeyError that bypassed both failure_policy="continue" and the _emit trace
    event. The projection is now non-raising and an explicit presence check raises a
    StepError naming the step + missing keys inside the try, so trace and
    continue-policy semantics apply. Latent (no shipped pipeline...
Read more

v0.8.0

Choose a tag to compare

@muratovv muratovv released this 08 Jun 05:46

[0.8.0] - 2026-06-07

Added

  • compute_usage step + usage.json per-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/v1 report: measured always-on budget (first cache_creation proxy), an
    ordered event timeline (skill-body loads via Skill tool_use, reference Reads
    of */references/*.md + SKILL.md, tool calls with is_error, stop-hook
    firings), aggregates with tool success-rate, and sub-agent sidechain linkage
    (detect + link by sessionId/sourceToolAssistantUUID, no per-event token
    merge). The report also self-describes its ai-hats context — role /
    provider / exit_code copied from the session's metrics.json (so the
    comparison sibling pairs sessions by role and "what went wrong" debugging reads
    it in one place); when role resolves, a static costs.py per-component
    always-on breakdown is attached under always_on.static for a measured-vs-
    static cross-check. The pure parse_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. The ComputeUsage step is the thin
    live driver — sibling of make_audit, same post-session JSONL, wired right
    after it in both finalize-hitl and finalize-subagent — so every new
    session writes <session_dir>/usage.json alongside audit.md/metrics.json.
    Reproduces the HATS-578 finding automatically (skill-BODY loads are rare —
    ~20% of sessions; backlog-manager + self-retrospective dominate). Per-event
    token attribution is a documented reconstructed heuristic (per-message usage
    is a per-turn total); unattributable events keep tokens_delta = null, never a
    magic 0 (honors rule_composition_value_contract §3).
  • devils-advocate skill + conditional "Approach & counter" plan section
    (HATS-621, M3 of HATS-629) — the value-counter stage of the plan-gate. A new
    required=False Approach & counter section sits between Requirements and
    Scope & Out-of-scope (PLAN_SECTIONS); the engine never blocks execute on
    it (the "non-trivial plans fill it or write explicit N/A" norm is behavioural,
    carried by the skill + companion HYP). The devils-advocate skill 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 into trait-agent. plan-gate documents the
    requirements-interview ⇄ devils-advocate → design-minimalism flow, with
    cross-refs in both sibling stages. Catches "right scope, wrong direction" — the
    failure mode neither requirements-interview (WHAT) nor design-minimalism
    (HOW MUCH) catches.
  • plan-discipline skill (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 to plan-gate for section filling; the engine
    per-section gate (HATS-635) remains the enforcement backstop. Wired into
    trait-agent. Closes the plan-mode→.claude/plans salvage loophole left after
    HATS-637 at the discipline layer. backlog-manager and rule_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 tracker plan.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-protocol mandates a verification_protocol
    field on companion HYPs, but hyp create exposed no flag and
    rule_backlog_discipline forbids editing hypotheses/*.yaml directly —
    so HATS-616 had to fold the protocol text into --success-criterion. The
    Hypothesis model is already extra="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
    by reflect/session-reviewer handoff (HATS-534).
  • dev-web role — 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 mirrors dev-python + dev::python:
    a single role dev-web over one gear dev::web (not a go-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_complete gates:
    npm run lint/test/build + npx playwright test. Companion HYP-056
    tracks the expected behavior shift. Per-source licenses + attribution
    recorded in each skill's metadata.yaml upstream: block.
  • Skills can declare provider runtime hooks (runtime_hooks: in a
    skill's metadata.yaml, HATS-597 / HATS-601). Mirrors the git_hooks
    open registry: a composed skill declares hooks keyed by Claude event
    (v1: PreToolUse, PostToolUse), each row {matcher, script}. On
    self init / self update the 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
    ClaudeProvider wires one managed .claude/settings.json entry per
    (event, skill, matcher), tagged ai-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). Hardens ai-hats self update /
    non-greenfield self init against data-loss regressions of the
    class that produced the proxmox failure mode (user-authored
    .agent/hooks/pre_bash_secret_guard.py silently 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 with Recovery: 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
      managed library/hooks/ namespace. The matching
      .claude/settings.json PreToolUse 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 to user-hooks/
      next bump heals stuck states inherited from prior versions
      transparently.
  • Install diagnostics in ai-hats config status Health section
    (HATS-497). config status now 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),
    Library path, Resolved via (heuristic over AI_HATS_VENV env >
    ai-hats.yaml venv_path > default), and Repo HEAD (editable
    installs only — short SHA + branch + clean/dirty). Pip-managed
    direct_url.json (PEP 610) is the source of truth for Source;
    HATS-496's --revision writes 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 setup codifies the
    two-venv pattern (AI_HATS_VENV env 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...
Read more

v0.7.0

Choose a tag to compare

@muratovv muratovv released this 23 May 19:19

[0.7.0] - 2026-05-23

Composition-and-customization release. MAJOR bump driven by three shifts:

  1. v0.6 → v0.7 layout migration is now folded into self update /
    self bump; the standalone self migrate-v07 verb is retired
    (Migration: under Removed).
  2. User-level overlays at ~/.ai-hats/customizations.yaml ship as a
    first-class layer; personal-workflow migrates there
    (Migration: in the ✨ BREAKING section).
  3. Role architecture splitsassistant = 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).
  • 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.pydev-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-tags

Compose 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 bump

Worked example: docs/how-to-extend.md §"Migrating from a removed
built-in component".

Added

  • HATS-445ai-hats execute --prompt <name> resolves
    initial_injections/<name>.md through the full library_paths chain.
    Unlocks shell-alias custom verbs: plugin authors ship a role +
    injection and wrap ai-hats execute in 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-444docs/INDEX.md is 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 staging INDEX.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_write forbids gh pr create/close/merge, gh issue comment, gh release create,
    git push, TaskCreate without 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-reviewer cites source-tags when filing proposals
    (framework vs user vs project). Closes the observability gap created
    by HATS-421.
  • HATS-408ai-hats self migrate-v07 one-shot safe migration from
    v0.6 to v0.7. Inspects on-disk artefacts, diffs each vs composition
    baseline, refuses on user edits (--force bypasses). 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 update hint 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-415ai-hats self update and self bump self-heal
    v0.6 → v0.7 layouts inline. Safe-to-delete v0.6 files (bytes match
    baseline) are swept transparently; user-edited files raise
    AssemblyError with 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 materialises priorities.md / role.md /
    traits/*.md / rules/*.md / skills_index.md. write_canonical
    emits only the imports.md aggregator. Providers' build_override
    renamed to build_session_prompt.
  • Migration: HATS-407ai-hats role set <name> is yaml-only
    (writes default_role: to ai-hats.yaml). Removed
    ai-hats self rollback
    — yaml-only config means git checkout ai-hats.yaml is the recovery path. Users scripting self rollback
    should switch to git checkout.

Removed

  • Migration: HATS-415ai-hats self migrate-v07 CLI command
    removed. Its logic lives inline in Assembler.bump() and surfaces on
    self update / self bump. Flags re-homed: --force
    --migrate-force, --check-branches kept. --no-commit has no
    analog. Migration: drop the self migrate-v07 invocation, run
    ai-hats self update — sweep auto-applies on a v0.6-shape project.

Fixed

  • .gitignore legacy block sweepai-hats self bump / self update now removes the pre-HATS-317 # AI-HATS:START..END managed
    block from user .gitignore files. 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). New Assembler._strip_legacy_managed_block() strips the
    block + one preceding blank-line separator, idempotent, respects
    manage_gitignore = False. Delivery pattern matches HATS-413:
    persisted on self bump only, no rewrite-on-read. Dogfooded on
    ai-hats's own .gitignore (121 → 48 lines).
  • ai-hats self update — short-circuit pip install when installed
    SHA already matches remote master. 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_refs walk no longer looks like a hang.
  • HATS-457ai-hats wt merge drift guard (HYP-017). Between
    wt create and wt merge the base branch could advance — another
    agent's merge into local master, or origin/<base> pulled in
    commits — and the pre-merge grep-verify became silently stale.
    WorktreeManager.create snapshots base SHA; wt merge does a
    best-effort git fetch and refuses with WorktreeDriftError on
    divergence. New --accept-drift flag (separate from --force
    two checks, two flags). Legacy state files gracefully skip.
  • HATS-452 — composition / pipeline value contract. Bare ai-hats
    was writing a prompt.md missing the merged role/trait injection —
    16k chars of behavioral guidance never reached the agent. Root cause:
    compose_role returned {"system_prompt": ""} for missing role;
    WrapRunner.run_session accepted the empty string and replaced the
    freshly-composed list with [""]. Four-layer fix per
    ADR-0005:
    immutable CompositionResult, funnel drops None at merge bo...
Read more