# Changelog What each release of attest_tag changed, and what, if anything, an upgrade asks of you. Each [GitHub release](https://github.com/Attest-Tag/attesttag/releases) opens with its section of this file. Versions follow [semantic versioning](https://semver.org). Before 1.0 that means: - A **patch** release (0.1.1) only fixes things and never migrates the database. A deployment pinned to its minor version (`ATTEST_VERSION=0.1` in compose's `.env`, `image.tag: "0.1"` in the chart) can restart onto one without reading anything first. - A **minor** release (0.2.0) may migrate the database or change something you have to act on, and its section here says which. Migrations apply by themselves at startup and there are no down migrations, so back the database up before a minor upgrade: going back means restoring that backup. ## 0.3.0 (2026-10-06) The first release with images: `ghcr.io/attest-tag/attesttag:0.3.0` for linux/amd64 and linux/arm64, the fix-job worker images beside it, the Helm chart at `oci://ghcr.io/attest-tag/charts/attest-tag` and binaries for Linux and macOS, all signed with cosign. Pin the minor (`ATTEST_VERSION=0.3`, or `image.tag: "0.3"`) to take its fixes and no migration you did not read about. From here on these are built from `main` only: the release workflow refuses a tag that `main` does not contain. → [Deploy](guide/deploy.md) **Code review shows on the pull request while it works.** → [The reactions and the check](guide/code-review.md#the-reactions-and-the-check) - **Eyes, then a rocket.** A live review puts an eyes reaction on the pull request as it starts and swaps it for a rocket once it is posted; a review that fails or is cancelled only takes the eyes off. - **An *attest_tag review* check.** In progress while the review runs, then a success with the score and the open findings — or neutral, skipped or cancelled when it was not posted, never a failure, so requiring it blocks nothing. It needs the App's **Checks: Read and write** permission, which is optional: an installation without it gets the reactions alone. - **Listed among the reviewers.** A first review with nothing to say inline now posts a review of one line — the commit, the score and where the findings are — so the App appears under the pull request's reviewers; later runs with nothing new inline still post none. - **A fix job's pull request is reviewed.** A pull request the App opened itself is no longer skipped as a bot's: it is reviewed once somebody marks it ready, as anybody's draft is. **Code review reviews the bots you name.** A pull request a bot opened was always skipped. *Bots to review*, a new list in the review settings, names the bots whose pull requests are reviewed without anybody asking — by login, with or without `[bot]`, or a glob, and `*` for every bot — and adds up from the connection down like the other lists. Any other bot's pull request is still skipped, and the authors to skip still apply. → [Settings and their defaults](guide/code-review.md#settings-and-their-defaults) **Fix jobs say a check did not finish, not that it failed.** A suite or a build the worker's sandbox killed partway — most often out of memory, on a large repository — was reported as failing: the pull request opened with "Tests still fail after this change", its table counted one failure, and the coding agent was told the suite failed before its change, "that may be the bug". It is now *did not finish (killed: out of memory)* everywhere — the pull request, the thread, the console and the agent's brief — and the headline also says whether a failing check passed before the change, fails worse after it, or was failing already. → [Fix jobs](guide/fix-jobs.md) **Fix jobs check their change with a narrower run.** The coding agent was told to run the whole build, lint and test suite itself before it stopped, although the harness runs them again after it. On a large repository, one job spent some 60 of its 150 turns running them, and a type check, again and again, each turn re-sending the whole conversation so far. The brief now gives each check's time from before the change and asks for the narrowest run that covers the change — the tests for the files it touched, the linter on just those files. It runs a whole command only when it passed in under two minutes, and spends no turns making a command run that the sandbox stops. → [Fix jobs](guide/fix-jobs.md) **A member who is not a Slack admin can no longer disconnect their workspace by pressing Add to Slack.** Slack hands back the same bot token for an app already installed in a workspace, and a refused install (the person is not a workspace admin) gave that token back to Slack — revoking the one the organisation that installed the bot was running on, and taking the bot out of the workspace for everybody in it. A refused install now gives a token back only when no organisation here uses that workspace; on one another organisation holds, it says so and points at an invitation. Signing up with Slack from a workspace another organisation already connected no longer carries straight on to Add to Slack either: the setup walk shows the workspace is taken and offers to ask for an invitation. And Slack's `tokens_revoked` disconnects a workspace only when it names the bot's token, not when people give back tokens of their own. A workspace this already disconnected needs one reinstall by a Slack workspace admin from the organisation that owns it. **A review finding can be fixed on the pull request.** Somebody who can push to the repository ticks the box on a finding's comment, replies `@ fix` in its thread, or comments `@ fix` (or `@ fix p1`) on the conversation, and a fix job commits the change to the pull request's own branch — one more commit, pushed as the App, never forced, no new pull request — then answers with the commit and what the checks said, and the review of the new head closes what it fixed. A change that breaks a check that passed before it is not pushed; its diff comes back with the answer. Pull requests from forks are refused, and so are people GitHub says cannot push. *Fixes* is a new review setting, on by default; turning it back on needs Manage connections. A reaction cannot ask for a fix: GitHub never delivers reactions to an App. → [Fixing a finding](guide/code-review.md#fixing-a-finding-on-the-pull-request) **The fix-job worker on Google Cloud gets 8 GiB**, up from 4, beside its 2 CPUs: Cloud Run keeps a job's files in memory, and a large JavaScript repository's suite and build did not fit. Run `deploy/gcp/worker.sh` again to apply it (`MEMORY=4Gi` keeps the old size). The other platforms' defaults are unchanged. **Fix jobs keep their prompt cache, are billed exactly, and stop at their budget.** A coding agent re-sends its whole conversation every turn, so a long job reads millions of input tokens, nearly all of them the same prefix again. → [What a job costs](guide/fix-jobs.md#what-a-job-costs) - **A model proxy in the worker.** The engine calls a proxy on the worker's loopback instead of the provider, with a token for that port: the model key no longer enters the sandbox. On OpenRouter every call carries the job's `session_id`, which keeps the job on the provider holding its cache, and asks for usage accounting, so each answer says what it cost, what came from cache and who served it — the real bill, even on the shared key. At the job's budget the proxy stops the engine and what it changed is opened as a draft. - **Providers on purpose.** A new `worker_providers` setting (Settings → Workers → Providers) names the OpenRouter providers a job asks for, in order. - **The cache share everywhere the cost is.** The cached part of the input is stored on the job and shown in the Slack report ("9.8M in (90% cached) / 74k out"), the console's job page, the reply on a pull request a fix was pushed to, and a new **Cost** section in the pull request a job opens. - **pi as a second coding agent** (Settings → Workers → Coding agent). Seven tools, a short system prompt and its own compaction; on the fix-job evals it read about a sixth of the tokens Qwen Code did for the same correct fix. → [Engines and long jobs](guide/fix-jobs.md#engines-and-long-jobs) - **Long jobs carry less.** Both engines compact against a 160,000-token window instead of the model's million; Qwen Code runs without its sub-agent, web and memory tools and cuts tool output at 6,000 characters. The brief lists the files the request names as they are in the clone, and tells the engine to batch reads and read each file once. - **Node builds get the memory the container has.** Repository commands run with a V8 heap of about 60% of it, where a large front end's build ran out of Node's default. - **Fix-job evals.** `evals/fixjobs` runs small tasks as real jobs and grades them with hidden tests, to compare engines, models and providers on what was right and what it cost. → [Fix-job evals](evals/README.md#fix-job-evals) **Models that will not be told which tool to use still answer.** Some endpoints refuse a `tool_choice` other than `auto`. Meta's Muse models refuse both `none` and a named tool. Claude Sonnet and Opus 5.5 and Qwen 3.8 Flash refuse a named tool while they reason, and OpenRouter answers a 404 when no endpoint of a model takes `none` (Llama 4 Maverick). A run on one of them failed at the step that asked: the first round of "check our docs" or "search the web", the landing round of a routine or reply that had spent its budget, and the last round of a code review. The request is now sent again without the choice, which every caller already handles: a tool the model skipped is run for it, and a call made while landing is dropped. The refusal is remembered per model and per kind, so a model that takes `none` keeps it. **A provider that fails partway through is retried, not reported as "no choices".** When an upstream times out after OpenRouter has started its reply, the failure arrives as a 200 with an error in the body. That turned into "model returned no choices" and was never retried. It is now an error carrying what the provider said, and a timeout, a rate limit or a 5xx is tried once more. **Google Gemini and DeepSeek as your own model key, and Gemini 3's tool calls on Google's own endpoint.** Settings → Models → *Your model key* offers Google Gemini and DeepSeek beside OpenAI and OpenRouter, each with its address filled in. → [Its own model key](guide/plans.md#its-own-model-key) - **Tool rounds go back as the endpoint wrote them.** Gemini 3 at Google's own endpoint refused every round after a tool call ("Function call is missing a thought_signature"), so any question that needed a tool failed there. A tool round now carries back what the endpoint handed out with its calls: the signature Google puts on each one, OpenRouter's `reasoning_details` (Gemini's signatures there) and DeepSeek's `reasoning_content`. OpenAI's API writes none of these, and is sent none. - **Model lists and prices.** Google's list drops the `models/` it puts in front of every id. A Gemini model is priced from the catalogue as `google/` and a DeepSeek one with a version in its name as `deepseek/`; DeepSeek's unversioned names (`deepseek-flash`) are aliases and stay unpriced. - **DeepSeek has no embedding model**, so document search is off while a DeepSeek key is in use. Its models refuse a named tool while they reason, which the change above already recovers from. **A custom review type can be deleted** once no branch rule names it. The row and its versions are kept (migration `0033_review_type_deleted` marks it), so past runs still name what they ran with and its key stays taken; a built-in type or its copy cannot be deleted. → [Code review](guide/code-review.md) Upgrading: migrations `0032_job_cached_tokens` and `0033_review_type_deleted` add columns and apply themselves at startup; both are additive, so the old version keeps working while the new one rolls out. The fix-job proxy, pi, and fixes on the pull request need the worker image from this release (`deploy//worker.sh`, or the published `attesttag-worker:0.3.0`): an older worker records no cached tokens and refuses a fix-on-the-pull-request job at its clone. For the code review check, add *Checks: Read and write* under the App's *Permissions & events* at GitHub; each account that installed it is then asked to accept, and its next review has a check. ## 0.2.0 (2026-10-04) Like 0.1.0, the source alone: no images, Helm chart or binaries are published for it, so build from the checkout as the deploy guides say. **Code review.** attest_tag reviews pull requests on GitHub, through the GitHub App it already installs. → [Code review](guide/code-review.md) - **One review per run, as a comment.** Inline findings, each checked by Go against the code and, when a model raised it, by a second model call that tries to refute it, and one summary comment edited in place, with a confidence score from 0 to 5 that Go computes from the open findings. It never approves or requests changes, and never runs the pull request's code. - **Settings by connection, group and repository**, under Automation › Reviews. An installation added there starts in shadow, which records the whole review in the console and writes nothing to GitHub. A single value comes from the nearest level that sets it; lists add up. - **Review types and branch rules.** General, Security, Tests, Performance and Release summary ship built in; a team can edit them, add its own, keep every version, and try an edit on a pull request before saving it. Ordered rules on the base and head branch choose which types a pull request gets, and label rules add types for a label on it — put on later, a label reviews what it adds. - **Skills.** A type can follow up to five skill folders — a `SKILL.md` and the Markdown beside it — from the repository under review (read at the pull request's base), one of the organisation's connected repositories, or any public repository, pinned to a commit if you like; a finding that rests on one cites it, and the summary names the skills the review followed. - **Commands and replies.** `@ review`, `full review`, `status`, `help`, `pause` and `resume` from the repository's own people, and a verdict on every reply in a finding's thread, which can withdraw or downgrade the finding within limits on who may move a P0 or P1. A finding that is fixed or withdrawn has its thread resolved on GitHub where the App's token is allowed to (best effort; the connection card says when GitHub refuses). Automatic reviews of one pull request pause by themselves after five, and the summary says so. - **Re-reviews close what a push fixed.** Each later review checks the earlier findings at the new head: a fixed one moves to the summary's *Fixed* with *Fixed in* in its thread, one whose code is gone to *Outdated*, and a "fixed in" claim is checked rather than taken at its word. - **Start review** in the console, with an estimated cost, and **Run again** on a past review. A paused pull request says *Paused* there, with **Resume**; a branch rule takes **Labels**, and reads `label perf → +Performance`; and where the plan has no code review the Reviews page says which plan has it, with the way to upgrade, while `CODE_REVIEW=off` takes the page out of the console. - **Reviews asked for in Slack or Teams.** "review acme/web#12" in a channel starts one through the bot's GitHub tools, for a repository that channel is granted in Access bundles and nothing else, under the same gate, budgets and throttles as the console's Start review. → [Code review](guide/code-review.md#from-a-slack-or-teams-channel) - **Announcements in a chat channel.** A level's *Channel*, or a branch rule's *Notify*, names a Slack or Teams channel the bot is in; each pull request gets one message there, edited as reviews finish and when it is merged, with each of those a reply in its thread. Choosing it needs `connections.manage`, and a channel shared with another organisation (Slack Connect) is never posted in. - **Starts, failures and Notify on.** A review's start edits the message to *Reviewing `abc1234`…* with no reply and without holding the review up; a failed review puts it back and says which kind of failure, never the error; a review the budget, a pause or the plan stopped is said once. *Notify on* picks which of starts, results, failures and merges a channel hears, inherited whole, and needs only `reviews.manage`. A console link that names a settings node opens on Settings. - **Adding and removing repositories.** *Add repositories…* on a connection lists what its installation reaches at GitHub and saves the ones ticked; *Remove from reviews…* takes one out of code review while its connection stays for Slack tools and fix jobs, and *Restore* puts it back. - **The console assistant on Reviews.** With a type or a level open, ask it to add, change or switch off a type's rules, make a new type, or add, change, move or remove a level's branch rules. It proposes a card and writes nothing; Confirm makes the page's own save under the person's own permissions, and is refused with nothing written if the type, the type a new one copies, or the rules changed since; the page then reads again and shows the change that won. It names who wrote a rule learned on GitHub, while the rule keeps their words, and which skills a type follows; skills, models, budgets and posting stay on the page. A card is confirmed only in the organisation it was proposed in, draws every path pattern it adds or removes and the model and budget a copy brings, and says when Confirm uses your `connections.manage`; Activity shows the assistant's tool-call arguments only to a reader who also holds `audit.view` and what the call was about, and only such a reader may search what it was asked. The assistant is offered to anyone with `reviews.view` and `reviews.manage` now, as well as to channel and approver admins, and what it is told about code review is sent from the Reviews page only. → [Changing types and rules from the assistant](guide/code-review.md#changing-types-and-rules-from-the-assistant) - **A settings save never undoes a newer one.** If somebody saved a level's rules, or the rules it inherits, since the page read them, Save is refused and the console keeps your edits over theirs; through the API, `digests.branch_rules` sent back as `expect` does the same. A save of any field that lands while another save of the level is being checked refuses that one too, rather than being put back by it. - **Money of its own:** a maximum per review, a monthly review budget and a daily cap, each held before a review starts, and a *Code reviews* switch on an organisation's own model key. `CODE_REVIEW` says which organisations have it at all: every one (`all`, the default), those on the pro or enterprise plan, the enterprise plan only, or none (`off`). - **The one write that never goes through Confirm.** A review on a repository set to *Live* is posted with nobody pressing anything, and no allow rule or `auto` setting is involved; *Live*, the model and the money need `connections.manage`. [Guardrails](guide/security.md#code-review-posts-without-a-confirm) says what stands in for the Confirm. **Security fixes**, found by an audit of the code review change, of which the first two were in 0.1.0 too: - Attaching a GitHub App installation to an organisation now needs its account's owner, or somebody with admin on every repository it covers. It used to need only that the installation was visible to the person, so an organisation's member or outside collaborator could attach one nobody had claimed yet, with its tokens. - A member without `audit.view` can no longer search the console assistant's questions and replies, which told them, a guess at a time, what a redacted reply said. - A label rule no longer hides a branch-rule change from the check that keeps live posting with `connections.manage`. What an upgrade asks of you: - Five migrations, `0027_code_review`, `0028_review_lane`, `0029_review_snippet`, `0030_review_notify` and `0031_review_type_skills`, all additive and applied at startup. Back the database up first, as for any minor release. - Nothing more, unless you want code review. Then set `GITHUB_APP_WEBHOOK_SECRET`, turn the App's webhook on at GitHub with the five events [configuration](guide/configuration.md#the-apps-webhook-and-permissions-for-code-review) lists, and add an installation under Automation › Reviews. - Connecting a GitHub App installation (Connect GitHub, or the install redirect) takes the account's owner or an admin of every repository it covers; somebody who only requested the install is told to ask one of them. Installations already connected are not affected. - Two new permissions: `reviews.view`, which the viewer, editor and admin roles hold, and `reviews.manage`, which editor and admin hold. A role you made yourself has neither until somebody ticks them. ## 0.1.0 (2026-09-28) The first release. It is the code that has run one organisation's Slack for months and runs the hosted service, with everything that was specific to that deployment turned into configuration. It is the source alone: no images, Helm chart or binaries are published for it yet, so build from the checkout as the deploy guides say (`docker build -t attesttag-local .`, or `make build`). - **Answers in Slack and Microsoft Teams**, in threads and DMs, on open-weight models through any OpenAI-compatible endpoint (OpenRouter and GLM-5.3-Flash by default). - **Tools with no setup:** Slack history and search, your documents, web search and fetch, memory per channel or per person, and routines it schedules when asked. - **Connections without handing over a key.** Credentials for thirty-odd services and remote MCP servers are sealed at rest and injected by a proxy at the network edge, so the model never sees one, and every write waits for somebody to press Confirm. - **Fix jobs:** an isolated worker clones a repository, makes the change, runs its tests and opens a draft pull request. It never merges. - **An admin console** for scopes, connections, documents, routines, activity, the audit log and budgets, with password, Slack, Microsoft or OpenID Connect sign-in and two-factor. - **A `/v1` API and an MCP server**, with keys that carry exactly their maker's access. - **SQLite or Postgres.** SQLite by default, replicated to any S3-compatible bucket if you give it one; Postgres when you want more than one instance. - **Runs anywhere a container does:** Docker Compose, a Helm chart, and scripts for Cloud Run, ECS Fargate and Azure Container Apps. Or run the binary itself. What is still rough, and what is wanted next, is in the README's [Status](https://github.com/Attest-Tag/attesttag#status).