Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

All notable changes to Druks. Versions follow [semantic versioning](https://semver.org);
while Druks is pre-1.0, a minor bump may break compatibility.

## [0.13.0] — 2026-11-05

### Breaking changes

- **An agent call reads, then acts on the box.** `Workspace` is a plain class
  that Druks builds as `workspace_class(subject)`: `setup()` reads rows before
  the box exists, `prepare()` acts on the box with no session bound, and Druks
  sets `host` in between. `Workflow.get_workspace_kwargs()`, `get_workspace()`,
  `Workspace.prepare_context()`, and dataclass workspaces are gone.
  `Host.save_files()` replaces `Workspace.save_files()`, `File.upload()` ships
  a file argument, and `set_git_identity()` takes no session. A read in
  `prepare()` or in a hook raises `SessionNotBoundError` instead of holding a
  connection through the run.
- **A `RepoWorkspace` subject names its `repo` and `ref`.** `ref` is the commit
  or branch to clone, `None` for the default branch, and a subject without it
  raises. `RepoWorkspace.branch` and `get_repo()` are gone.
- **A managed service's OAuth redirect is `<issuer>/services/<slug>/oauth/callback`.**
  The consent URL and the token exchange both carry it, and
  `Service.get_oauth_client()` derives it, so apps change nothing. Deploy only
  after the manager accepts the path. Self-hosted OAuth keeps
  `<endpoint>/api/oauth/callback`.
- **`Gate.answer()` requires `workflow=`.** Two workflow kinds on one subject can
  wait on the same gate name, and the answer now reaches the kind it names.
- **Short-lived tokens live in the vault, not in Redis.** Resource tokens, GitHub
  installation tokens, and OAuth access tokens are encrypted rows with an
  expiry. `GitHubAppClient` signs with the App key and `GitHubClient(token)`
  makes the installation calls; `GithubApp.get_client(owner_or_repo)` and
  `get_app_client()` build them. `get_mention_handle` is gone, and
  `evict_access_token` takes the session.
- **Druks is licensed under FSL-1.1-ALv2.** Self-hosting, changes, apps, and
  professional services stay free; a competing hosted service is not allowed.
  Each release becomes Apache 2.0 two years after it ships. `docs/licensing.md`
  has the details.

### Added

- **An app has a directory it may write.** `App.get_data_dir()` returns
  `<DRUKS_DATA_DIR>/apps/<name>` and creates it. A downloaded tool or a tool's
  state goes there, never under `~`.
- **A workflow can wait on an event without parking.** `self.wait(topic,
  label=, ttl_seconds=)` suspends the run while it reads as running, and
  `Workflow.answer(subject, topic, **facts)` answers it from a subscriber. A
  wait past its TTL fails the run with `WaitTimeout`, and
  `answer(subject, topic, failure=...)` fails it with `WaitFailed`.
- **App code gets its account.** `druks.accounts` exports `Account`,
  `AccountKind`, and `current_account`. A route takes the caller's row with
  `Depends(current_account)`, and a run carries `self.account`.
- **Jira events reach every app.** The Jira webhook moves into core and turns a
  Jira Automation issue into `ticket.transitioned`, with no Software Factory
  needed. The URL stays `/_external/jira/events/`.
- **Linear and GitHub issues publish `ticket.created` and `ticket.updated`.**
  Every `ticket.*` event carries one set of keys whatever the tracker:
  `source`, `ticket_id`, `identifier`, `title`, `url`, `project_name`,
  `labels`, and `actor_id`. Linear events add `team`.
- **GitHub issues publish `issue.opened`, `issue.edited`, and `issue.reopened`**,
  with `repo` and `number` beside the payload.
- **`druks doctor` names runs parked past the gate timeout**, in
  `stale_parked_runs`, so an operator can cancel them.
- **The sandbox image has `jq` and `file`.**
- **Python 3.15.**

### Changed

- **A workflow sandbox starts with a thirty-minute lease**, and an active
  workspace renews it every five minutes. An idle or dead worker lets its box
  expire.
- **A recovered agent call attaches to the agent the stopped process left
  running**, instead of starting a second one beside it.
- **The host takes the harness and the manifest, not a session**, so an agent
  run holds no database connection.
- **Software Factory** reviews a build's pull request with the standalone
  review and waits on it: `request_changes` returns to implement, up to
  `max_implementation_revisions`, and `approve` parks for review. The
  evaluator, `review_code`, and `journal.evaluations` are gone. A cancel or a
  close without merge returns the ticket to rest while it is in progress or in
  review, and a tracker reads a status name with `get_status_name`. A build
  finishes when GitHub merges its pull request, fails before review when the
  planner cannot plan, answers 409 with the owning project when a repository
  is already registered, and caps a ticket title at 200 characters on edit as
  on create.
- **The MCP server names its website URL**, and `chat_require_tag` describes its
  argument.

### Fixed

- **`Workflow.cancel()` and `answer()` bring their own session**, as `start()`
  does, so a workflow body can cancel a subject's run and start another.
- **Druks installs beside githubkit 0.17.** hishel is bound below 2, which
  removed the transport githubkit imports.
- **After-agent hooks run on boxes without Node**, and a run whose hook never
  ran fails instead of finishing. On Python 3.11 a hook's agent call runs in the
  step's own task, so it no longer raises `SessionNotBoundError`.
- **`druks check-app` accepts an app that imports `druks.services` first.** The
  notifications package init no longer imports the outbox.
- **DBOS logs no duplicate traceback** for a `FatalError` or `HarnessError` that
  Druks already records.
- **A call with an unusable subscription token is refused** before a box is
  provisioned.
- **A run waiting for Claude's usage window shows as running.**

## [0.12.0] — 2026-10-31

### Breaking changes

- **A service declares how it authenticates in `Authentication`.** This nested
  class names its ways with the kinds in `druks.services.auth`: `ApiKey`, `Jwt`,
  `Oauth`, and `SignIn`. The OAuth endpoints and options move there from the
  `Service` class. `api_key`, `private_key`, `client_id`, and `client_secret`
  leave `Settings`. `get_authorization`, `get_identity`, `get_consent_query`,
  `read_grant`, `is_grant_revoked`, `get_install_endpoint`, and `issue_token`
  move to `Authentication`.
- **A sandbox secret names a tuple of resources.** `SandboxSecret.resources` and
  `SandboxMcpServer.resources` replace `resource`.
  `GitHubClient.get_token_for_repos(*repos)` replaces `token_for_repo(repo)`. A
  sandbox's GitHub token reaches only the repositories that its workspace names.
- **A notification destination is a Slack channel.** The kind `slack_channel` and
  `channelId` replace `slack_webhook`, `url`, and `hasUrl`. Druks posts as the bot
  of the Slack card. A migration deletes each `slack_webhook` destination and its
  notifications, and clears the defaults that name one. Apprise is gone.
- **`.druks/software_factory/review.md` replaces `.druks/review/checklist.md`.**
- **`pr.opened` is a platform fact from GitHub's webhook**, with `repo`,
  `pr_number`, and `payload`. Software Factory records `build.pr_opened`.

### Added

- **An app can give an agent one more turn.** An `after_agent` hook runs when the
  agent ends its turn. If it returns text, the agent gets the text as one more
  turn. A hook can sit on the app's own agent, or on another app's agent through
  `Agent.get(id)`, so neither app imports the other. Druks discovers `hooks.py`,
  and `RepoWorkspace.get_diff()` gives a hook the agent's change.
- **A workflow can run a command in a VM.** `Workflow.exec(command, timeout=...)`
  runs it in a VM of the workspace and returns a `subprocess.CompletedProcess`.
- **An app can act on pull requests.** The GitHub webhook publishes `pr.opened`,
  `pr.synchronized`, `pr.edited`, and `pr.ready_for_review`.
  `GitHubClient.post_check_run()` posts a check run with annotations.
- **An app can cache a provider read.** `App.get_cached()`, `set_cached()`, and
  `clear_cached()` keep JSON values in Redis, and each value expires.
- **`druks.toml` can switch off an app or a service.** Set `enabled = false` in
  `[apps.<name>]` or `[services.<slug>]`. `druks setup` keeps both tables.
- **Druks can act as an installed Linear app.** The Linear card takes an install
  or a pasted API key. Druks then uses that credential for tickets and for
  Linear's MCP server.
- **A manager can own the Slack and Linear apps.** The card then shows **Install**,
  and Druks uses the manager's app for messages, button clicks, and account links.
- **Choose the Claude sign-in method.** `sign_in` in `[providers.anthropic]` is
  `oauth`, the default, or `setup_token`, a pasted `claude setup-token`.
- **A sandbox can load the apps of a subscription.** The **Include apps** setting
  is off by default. When it is on, a run loads the subscription's claude.ai
  connectors or ChatGPT apps.
- **The Slack bot introduces itself.** It posts a welcome when it joins a room,
  and before the first link that it sends to a person.
- **The agent can read files on a thread.** `chat_read_thread` lists the files on
  each Slack message, and `chat_read_file` puts one in the sandbox.
- **Chat reacts with 👀** to each message from Slack, GitHub, or WhatsApp when its
  delivery starts.
- **An MCP server can pin the scopes that a connect requests.** The **Scopes**
  button on the server's row shows what the server advertises.
- **The effort levels `xhigh` and `max`.** Codex runs `max` as `xhigh`.
- **A theme preference.** Each account chooses System, Light, or Dark under
  Settings → Preferences.
- **`ResourceError` keeps `status_code` and `reason`**, so an app can tell a
  refusal from an outage.

### Changed

- **A sandbox secret that needs a person fails the run at once.** The failure
  gives the reason, such as a GitHub App that is not on the repository. Druks
  needs a current Drukbox for this.
- **Every MCP tool failure is one JSON object:** `{"code", "message", "retryable"}`.
  A wrong call is `INVALID_TOOL_ARGUMENTS`.
- **Claude loads every Druks tool upfront.** The Chat agent searches for a
  connected service's tools before it says that it cannot reach that service.
- **Chat asks the agent once for a missing channel reply.** The person gets the
  failure line only when the second turn also ends with no text.
- **A new web conversation gets its title during the first reply.**
- **Activity shows every finish.** A routine start records no row.
- **A `[services.<slug>]` table that no installed app declares is logged and
  skipped.** Druks does not refuse to start.
- **The GitHub and Slack manifests ask for more.** GitHub adds Checks write and
  Repository creation write, and Administration drops to read. `druks doctor`
  names each permission that the App lacks. Slack adds `reactions:write`,
  `channels:read`, `groups:read`, and the Interactivity URL.
- **The Linear webhook is in core**, so a standalone app can read Linear facts
  without Software Factory. The endpoint does not change.
- **A Software Factory build creates its branch on GitHub first.** A branch name
  that exists gets the next number. A pull request from a fork never fills a work
  item.
- **A Linear ticket routes to its repository by its labels**, then by its project
  name.
- **Agent commits and pull request titles follow the repository's format**, when
  its `AGENTS.md` or contributing guide gives one.
- **A person's Linear sign-in asks for `read` and `write`.**
- **The Schedules filter leaves out builtin apps**, as the Dashboard does.

### Fixed

- **The image no longer trusts the `emSign ECC Root CA - G3` root.** Both certifi
  and `ca-certificates` drop it.
- **A Codex run waits for its MCP servers before its first turn.** A slow server
  was missing from the run.
- **A CI check that a path filter skips does not block an evaluation.**
- **The dispatcher's co-author line stays** when a commit has other co-authors.
- **The web manifest loads behind an access check that needs a cookie.**
- **The Atlassian directory entry uses `/v2/mcp`.** A connect to the old endpoint
  requested no scope.

## [0.11.0] — 2026-10-18

### Added

- **An app can keep resources at a provider**, such as a Cloudflare Worker, and
  get a token for each one. Declare a `Resource` class inside the service class,
  as you declare `Settings`. It has four steps: `create`, `update`, `delete`,
  and `get_token`. The app makes every other call with `get_token()` and the
  provider's SDK. When a manager manages the service card, Druks sends the four
  steps to the manager, and the app code does not change. A failed step raises
  `ResourceError`. `resource.get_secrets()` gives a sandbox the resource's token
  as a placeholder, such as `CLOUDFLARE_API_TOKEN`. `Resource` and
  `ResourceError` come from `druks.services`.

## [0.10.0] — 2026-10-18

### Breaking changes

- **A Bot's number is public or private.** `BotAccess.PUBLIC` replaces
  `BotAccess.OPEN`, and `BotAccess.PRIVATE` replaces `BotAccess.PAIRED`. On the
  wire, `botAccess` is `public` or `private`.

### Added

- **Reminders in chat.** Ask the agent to remind you or another person, one time
  or on a schedule. Druks sends the reminder word for word through the
  conversation's channel: web Chat, Slack, or WhatsApp. A reminder takes a
  delay, a time, or a cron in the installation timezone, and the agent gets the
  time on each turn. The agent tools are `chat_create_reminder`,
  `chat_list_reminders`, and `chat_cancel_reminder`. The Chat page shows each
  reminder.
- **WhatsApp groups.** A public number answers in a group after its admin tags
  it there. Then a message that tags the number or replies to it reaches the
  Bot. On a private number, only the tag of a paired phone reaches its agent.
  The agent reads the group with `chat_read_thread` before it answers, and tags
  a member with `@` and their number. A number linked before this release
  ignores groups until you link it again.
- **More for WhatsApp.** On a private number, an operator's agent can start a
  chat with a phone number through `chat_start_conversation`, for example to set
  a reminder there. A shared contact card reaches the agent as text.
- **GitHub Issues as a Software Factory tracker.** An issue that gets the trigger
  label opens a build. The five status settings are label names, and the done
  label closes the issue. A ticket key is `owner/repo#number`. The operator App
  must subscribe to the `issues` event.
- **An agent can drive a saved browser session.**
  `Agent(browser=session, browser_client=BrowserClient.PINCHTAB)` gives the call
  five browser tools through the Druks MCP server. The sandbox never holds the
  browser's endpoint, token, or profile. In app code,
  `BrowserSession.pinchtab()` borrows the session, and
  `BrowserSession(read_only=True)` lets only GET requests through.
- **A run waits for a sandbox slot.** When Drukbox is at capacity, the run waits,
  and the dashboard shows "Waiting for a sandbox slot". `[sandbox.host_limits]`
  in `druks.toml` sets the limit of each provider.
- **A default for Slack tags.** On the Slack pane of Chat → Channels, a person
  sets whether the bot answers only their tagged messages in new Slack threads.
- **A Jira service account can connect.** Druks calls Jira through the Atlassian
  gateway, so a scoped service-account token works.
- **A tool can depend on configuration.** A router dependency that raises
  `ToolNotAvailable` hides the router's tools from `/mcp` until its condition
  holds.

### Changed

- **A chat hears every run it started**, also a run that returns nothing. That
  report names the run's subject, so the agent can read the output there.
- **A chat channel gets only the reply after the agent's last tool call.** The
  notes between tool calls stay on the Chat page.
- **A chat turn with no connected provider fails at once.** On a channel, an
  operator's conversation gets the cause, such as "connect your Anthropic
  subscription". A Bot's conversation keeps the fixed reply.
- **The Chat agent answers with outcomes.** It keeps Slack and GitHub replies
  short, loads a tool's schema before its first call, and ends its turn after it
  starts a run. In a thread that other people read, such as a WhatsApp group, it
  does not list a person's connections unless that person asks.
- **An MCP tool call with an undeclared argument fails**, and the error names
  the argument.
- **The board ticket tools show only while Software Factory's tracker is
  `druks`.**
- **`software_factory_start` answers a tracker refusal with an agent error:**
  409 `TICKET_NOT_MOVABLE` or 502 `TRACKER_REFUSED`.
- **`get_gate` reads a gate that is answered on its source.** `answer_gate`
  still refuses it.
- **`druks sandboxes build` prints the progress of each template build.**

### Fixed

- **The browser template builds on any docker base image.** It failed on every
  Compose install.
- **A live chat session gets new instructions and MCP servers on its next
  turn.** It kept its first prompt and servers after a deploy, or after an
  operator connected a server. The session keeps its history.
- **A per-person OAuth MCP server does not break other accounts' runs.** A run
  whose account has not connected the server leaves it out.
- **A push that changes a `.druks` file reaches the next build.** Druks used its
  cached copy for 24 hours.
- **A build does not wait on GitHub's anonymous rate limit.** The App's git
  author lookup uses an installation token.
- **A directory upload to a sandbox does not hang** when tar writes many warnings
  or the stream stops.
- **A skill repository URL without `https://` installs.** For a private
  repository without the App, the error names that cause.
- **A long URL in a card description wraps.**

## [0.9.0] — 2026-10-12

### Breaking changes

- **`druks.toml` holds no secret.** Each secret comes from its environment
  variable, such as `DRUKS_SECRETS_KEY`, or from a file in `DRUKS_SECRETS_DIR`.
  The `[secrets]` table is gone, and Druks does not start when `druks.toml`
  holds a secret. `druks setup` moves each secret to the last section of `.env`.
- **`druks sandboxes build` builds the sandbox templates.** It waits for each
  build and fails with its error. `druks doctor` only reports the templates. Run
  the command after each deploy. Druks needs a current Drukbox.
- **The browser is a sandbox template**, like Chat. The browser image and the
  settings `sandbox.browser_*` are gone. A `[browser]` table holds
  `proxy_scope` and `timezone`, and `DRUKS_BROWSER_PROXY` names the proxy.
- **Registry settings live in `[sandbox]`:** `registry_host`,
  `registry_username`, `registry_password`, and `template_repository`. The
  `EXE_*` registry keys in `[sandbox.exe]` are gone.
- **Druks runs on DBOS 3.** Do not run a 0.8 process and a 0.9 process against
  one database at the same time.
- **`GITHUB_API_URL` is gone.** Every GitHub API call goes to
  `https://api.github.com`.
- **A directory of MCP servers replaces the registry search.** The dashboard
  offers a vetted list of hosted servers that sign in with OAuth.
  `DRUKS_MCP_DIRECTORY` replaces `DRUKS_MCP_TRUSTED`. Add a server that is not
  in the list as a custom server.
- **Software Factory does not merge on approval by default.** `on_approval` is
  `none`: Druks marks the pull request ready for review and ends the build. Set
  `on_approval: merge` in `.druks/software_factory/config.yml` to merge.
- **WhatsApp has one link.** Add a number, then pair a phone with the code. The
  separate path for your own number is gone. A code that the linked phone sends
  to itself makes that chat yours.
- **A sandbox setup script sits beside its module.** The path is relative to
  the folder of the module that declares the `Sandbox`, not to the app package.
  `Sandbox.package` is gone.

### Added

- **Phone calls.** Link a Twilio number to an app's Bot, and the Bot answers
  calls with a speech-to-speech model from OpenAI or Google. The Bot's tools
  work during the call, and the caller's earlier calls are context. Any operator
  reads a number's calls in the app's Channels tab. The calls server is a
  separate program with its own image, `ghcr.io/czpython/druks-calls`, and the
  default stack does not run it. `docs/deployment.md` describes how to add it.
- **The operator's Chat reaches more.** It reaches every enabled MCP server. Its
  sandbox holds the operator's own sign-in at each service with a `host`, so
  `git` and `gh` act as that person. The prompt lists the sign-ins it holds. A
  Bot still reaches only the Druks server.
- **An agent declares the service secrets its sandbox holds.**
  `Agent(secrets=(Acme.fields.api_key,))` gives the sandbox `ACME_API_KEY`.
  `Service.host` is the one host that the secret can go to.
- **An MCP server can use its service's credential.** `Service.mcp_host` names
  the MCP host that the service's identity signs in to. GitHub, Jira, and Linear
  set it. An account with a sign-in at the service needs no second consent. Other
  accounts of a pasted-login service connect their own grant, and the default
  account sends the login that `Service.get_authorization()` builds.
- **A header MCP server can hold one key for each person.** Each person sets
  their own key on the server's row.
- **Disconnect a service** in Settings → Services. Druks revokes its credentials
  and every account's sign-in to it.
- **Create the reviewer GitHub App from Settings**, like the operator App. Each
  service that declares a `manifest` gets **Create App**.
- **Managed services.** A `[services.<slug>]` table in `druks.toml` holds the
  plain fields of a service card, and its secrets are files in
  `DRUKS_SECRETS_DIR`. Druks syncs the card at startup and refuses edits to it.
  With a `[manager]` table, a manager can answer a service's API calls and sign
  its webhooks. The card then differs only in its `url` and its key.
- **A person can require a tag in a Slack thread.** The agent tool
  `chat_require_tag` turns off the untagged replies for that conversation.
- **The browser proxy can cover every launch.** `browser.proxy_scope` is
  `login`, the default, for the login window only, or `all` for every borrow too.
- **More page and service blocks.** An app with no landing page gets a platform
  home: one live table for each subject type. `Cards(layout="tiles")` crops each
  image to a square. `Columns(layout="split")` is a page of two panes: a list and
  what it opened. A service card renders its `description` as Markdown, and a
  field with a default can stay empty.
- **More author reads.** `Service.list_for_account(account_id)` reads an
  account's sign-ins without a scope declaration. `GitHubClient.create_reaction()`
  reacts to a comment.
- **Instances can share one host's Postgres and Redis.** `DRUKS_WEB_PORT` sets
  the web port. `DRUKS_DATABASE_POOL_SIZE`, `DRUKS_DATABASE_MAX_OVERFLOW`, and
  `DRUKS_DBOS_POOL_SIZE` set the pool sizes. Druks supports Postgres 16 and newer.

### Changed

- **A chat hears every run it started.** A run reports its result when it waited
  or returned something, and it always reports a failure. Web Chat hears its runs
  too.
- **A failed Chat delivery retries** up to five times, with a backoff. Then its
  messages end as `failed`. The page shows **Send again** on a failed,
  interrupted, or cancelled message. A Slack, GitHub, or WhatsApp conversation
  gets one short line that the message failed.
- **The login window runs Google Chrome as a plain process.** Playwright attaches
  only for a short time to add and read cookies, so a site sees no debugger.
- **OpenAI subscriptions connect with Codex device authorization.** Druks shows a
  code and a link. First, enable device code authorization for Codex in ChatGPT.
- **An agent call can run for 2 hours.** The Agents settings offer 7200 seconds.
- **The Dashboard shows your runs and the unattended runs**, not the runs of
  other operators.
- **A tracker build runs under the person who moved the ticket**, then the
  assignee, then the default account. A Jira rule sends
  `{{initiator.accountId}}` in the `x-jira-initiator` header.
- **An unlinked person in a Slack room gets the link in a direct message**, and
  the thread says so.
- **Claude Chat sessions have auto memory off.** Subject pages start with every
  transcript collapsed.

### Fixed

- **Only the owner of a connection can reconsent or revoke it.** A reconsent
  wrote the caller's grant into another person's connection, and Settings listed
  the connections of every account.
- **Only a reviewer who can write to the repository answers a build's review
  gate.** On a public repository, any account's approval could answer it.
- **The Chat agent reaches the Druks tools.** The bridge sent `Bearer Bearer`,
  and Druks refused it.
- **A Chat message is sent again when the bridge never got its prompt.** A turn
  never runs twice.
- **A new key, or a switch between harnesses on one key, gets a new sandbox.**
  A warm sandbox kept the old key entries, so Codex failed without
  `CODEX_API_KEY`.
- **A scheduled run gets its sandbox.** Drukbox refused its run id as an
  idempotency key.
- **A catalog MCP server with a static key can take its key.** Nothing could
  store it, so every run failed while the server was enabled.
- **`druks init-db` works from a wheel install.** The package carries
  `alembic.ini` and the core migrations. The developer command is
  `uv run alembic -c backend/druks/alembic.ini`.
- **WebSockets work behind an edge that rewrites `Host`.** Druks compares
  `Origin` with the host of `urls.endpoint`.
- **Chat and the login window work on a docker-sbx host.**
- **A link to a subject whose id holds `/` or `#` opens that subject.**

## [0.8.0] — 2026-09-27

### Breaking changes

- **App tables and subjects.** `druks.db.Model` replaces `druks.db.Base`. A
  `StoredSubject` adds the columns `created_at` and `updated_at`, so each app
  needs a new revision. `__str__` names a subject and replaces `get_key()`.
  `SubjectSummary` has no `title`, and runs, events, and the Activity feed
  record only the name. An identity-only `Subject` overrides `get_or_none(id)`
  in place of `get_for_subject_id()`.
- **Renamed author names.** `druks.db.fields` replaces `druks.secrets.fields`.
  `Webhook.get_delivery_key()` replaces `delivery_key()`. `Service.scopes()`
  replaces `required_scopes()`. `Workspace.get_secrets()` replaces
  `get_secret_refs()` and returns `SandboxSecret`. `Workspace.get_mcp_servers()`
  replaces `get_required_mcp_servers()` and returns `SandboxMcpServer`. Both
  types come from `druks.sandbox`. `Workspace.get_all_mcp_servers()` replaces
  `get_mcp_delivery()`.
- **Pages and routes.** `ui.GateControls(subject)` takes the subject, not a run
  id. `Table(empty=EmptyState(...))` replaces `Table(empty_text=...)`. Druks
  adds the app name to every app operation id, so an app writes the bare verb.
  An id that starts with the app name fails at boot.
- **A GitHub tag goes to Chat** and does not start a review by itself. The
  GitHub card needs the App's client ID and client secret, and the App needs the
  callback URL `<endpoint>/api/oauth/callback`. The fact `issue.commented`
  replaces `pr.commented`.
- **`SLACK_SIGNING_SECRET` is gone.** The signing secret on the Slack card checks
  the buttons in gate notifications.

### Added

- **Slack is a Chat source.** Write to the Druks bot in a direct message, or tag
  it in a room, and your own agent answers there, under your account and with
  your tools. In a room, the agent answers in the thread of the tag. For 24 hours
  after the last message, your untagged replies in that thread reach it too. A
  file you send becomes a Druks file on your message. Connect the Slack card
  under Connections → Services. **Create Slack App** opens Slack with the Druks
  manifest filled in. Each person links their Slack account under Chat →
  Channels. The guide is `docs/chat.md`.
- **GitHub is a Chat source.** Tag the operator GitHub App in an issue or pull
  request comment, and your own agent answers with a comment where the tag was.
  The agent decides what to do: it answers, opens a ticket, starts a build, or
  starts a review. Only a person who can write to the repository reaches an
  agent. Each person links their GitHub account under Chat → Channels, and gets
  their own conversation for each issue or pull request. In Slack and GitHub,
  the agent reads the thread with the `chat_read_thread` tool.
- **Chat runs on Codex and OpenCode** as well as Claude. Select the harness in
  Chat's row under Chat → Channels → Bots. A conversation that moves to another
  harness starts a new agent session and keeps its messages. OpenCode ignores
  the effort setting.
- **The chat agent gets the files people send** on WhatsApp and Slack. An image
  reaches the agent with its message. Any other file, except audio, reaches it as
  a link to a copy in the sandbox. A Bot's agent has no file tools, so it sees
  only the link and the file name. A voice note becomes text before its turn,
  through the new **Speech To Text** card. The card takes any server that speaks
  the OpenAI audio API, such as OpenAI, Groq, or a local server. Without the
  card, or for a note over 25 MiB, the agent asks the person to write instead.
- **App tables have a `Model` base.** It names the table `<app>_<class>`, and
  has `create()`, `save()`, `delete()`, and the reads `get()`, `get_or_none()`,
  `all()`, and `filter()` by field. `get()` raises `ObjectNotFound` from
  `druks.exceptions`. A route answers it with 404, and a page shows an empty
  state with a link back. A class can declare its order:
  `class Report(Model, ordering=("-created_at",))`. A `StrEnum` or `Literal`
  column is text with a CHECK of its values. A `list` or `dict` column is JSONB.
  `druks makemigrations` names an app's revisions `<name>_0001`, `<name>_0002`,
  and so on.
- **A `StoredSubject` needs less code.** Its `__tablename__` is optional, and
  its board shows its newest hundred rows, so `list_summaries()` is optional.
  Its default name is its type and id, such as `note 7`.
- **Apps need less code in other places.** `ui.SubjectStatus(subject,
  working=...)` shows where the work on a subject stands. The shell links each
  page that follows one subject to that subject's timeline.
  `ScopedService.connect_url` is where an operator connects the service.
  `Agent(include_mcp=False)` gives the call no MCP server. `Workspace.get_env()`
  adds environment variables to every agent call. A service overrides
  `get_consent_query()` and `read_grant()` for a provider with a different OAuth
  shape. `druks.services` exports `Connection`, and `druks.workflows` exports
  `GateTimeout`.
- **`druks check-app <name>`** loads one installed app and checks its subjects,
  pages, operations, and routers. It needs no database.
- **A custom MCP server can authenticate with a secret header**, such as
  `x-api-key`. The form offers Bearer token or Header. A migration moves each
  stored bearer token to the same header storage.

### Changed

- **A failed run tells its conversation**, on every channel, also when the run
  did not wait. The agent says so in one short line, without error details, and
  does not retry.
- **A service card keeps what you do not enter again.** On a connected card, a
  blank secret keeps the stored one. A new paste revokes the linked accounts
  only when the client ID changes. The error names each missing field.

### Fixed

- **`druks.workspaces` and `druks.sandbox.client` import on their own.** As the
  first import in a process, each failed with a circular import.

## [0.7.0] — 2026-09-23

### Added

- **Chat.** Talk to an agent that acts on Druks as your account. A conversation is
  a live agent session in a sandbox that your account leases, not a workflow. Send
  stays available during a reply, with Stop beside it, and a queued message waits
  its turn. After a restart, Druks runs an interrupted delivery again. The list has
  title search, pins that stay with your account, and day groups in your timezone.
  Chat runs on Claude in this release. Its harness, model, billing, and effort come
  from its row in Chat → Bots. The guide is `docs/chat.md`.
- **WhatsApp is a Chat source.** Connect a WAHA service under Connections →
  Services, then link a number from an app's Channels tab with a QR code. A person
  who writes to the number gets their own conversation, and Druks sends the reply
  through WAHA. The person who holds the number's phone is its admin. A run that
  waits asks that admin in the phone's chat with itself, and only the admin can
  answer. A message typed on the phone pauses the chat for four hours. An operator
  links their own number under Connections → Accounts. WAHA must run the NOWEB or
  GOWS engine and reach `/_external/waha/events/`. Set `urls.webhook_host` or
  `urls.endpoint` before you link a number.
- **An app can declare a Bot.** `Bot(prompt, user_tools, admin_tools)` from
  `druks.agents` answers the numbers linked to the app. A route tagged `bot` is a
  tool of the Bot, and it takes the person who writes as `user: BotUser`. A run
  that a bot tool starts remembers its conversation, so `self.review()` asks the
  number's admin. The Bot has a row in the app's Settings → Bots as `<app>.bot`.
- **A `YesNo` gate.** `druks.workflows.YesNo` parks with the actions `yes` and `no`
  and an optional `note`. A gate's own fields shape its review: the note box shows
  only when the gate declares `note`, and the values of `action` are the controls
  when the ask names none. The review shows the question in the ask's `label`.
- **A page can put actions on more blocks.** A `Callout` takes `controls`, a `Form`
  takes `extra_actions`, a `Table` takes `select` with row `actions`, a
  `StatusValue` takes a `link`, and a `ControlsValue` puts actions and links in a
  cell, a fact, or a list item. An operation that answers `{"url": "https://..."}`
  sends the browser to that URL.
- **A skill collection installs from a private repository** when the GitHub App is
  installed on it.

### Changed

- **Activity is compact and app-scoped.** Rows take two lines under day headings,
  and a panel shows the details. Filters and the date range live in the URL. A type
  belongs to its app. Search matches the recorded work key or title. Rows show work
  titles, PR references, review summaries, and failure causes. A spent budget
  records as `spend_limit`, and the run fails without a quota wait. The live stream
  resumes from a transaction snapshot, so a late commit does not disappear.
- **A subject's stable handle is `key`.** `get_key()`, `subject.key`,
  `SubjectSummary.key`, and `subjectKey` on the wire replace the label. A migration
  renames the column. Rename an override of `get_label()` to `get_key()`.
  `get_summary()` projects the id and the key; override it for a title. An
  announcement that names `title`, `run`, or `kind` raises `WorkflowError`.
- **A `Choices` source returns dicts.** Each choice is `{"value", "label"}` with an
  optional `group`. This replaces `(value, label)` pairs. The select is searchable
  and grouped. A stored value that the source does not list shows under
  Unavailable choices, and save and `druks doctor` reject it.
- **Schedules is a full-width page.** Rows group by app. Preset and pause changes
  save at once. Each row shows a next-run estimate and up to eight recorded
  invocations. Run now queues one invocation, and a paused schedule stays paused.
- **Software Factory pages are easier to scan.** Projects has collapsible headings,
  flat repository rows, and an inline repository profile. Overview has compact
  desktop rows. A retry shows as a continuation of its build, with the summed time
  and cost, and Retry works only on the latest run of a work item. The work item
  shows a finished call's artifact as a tab beside the transcript, and the
  evaluation is that artifact. An external review request shows as information
  with a link to its source.
- **The evaluator is quieter.** It files no follow-up tickets. Its findings stay in
  `review_notes` and in the GitHub review, and the operator decides what becomes
  work. After the first round, a review says only what changed. A rework push
  resolves the review threads it addressed. A review request keeps the note around
  the mention. A retry with no failed step forks from the last declared step. Jira
  gets no transition when the status already matches.
- **The GitHub App needs Administration: write** to create a repository from a
  template. An existing App must add the permission and accept it on the
  installation. The manifest routes now serve under `/api/core/services/github/`.
- **`urls.webhook_host` is the HTTPS host of this installation's `/mcp` endpoint.**
  When it is empty, `urls.endpoint` is the base. `docs/deployment.md` describes an
  installation behind an external proxy.
- **Connections explain more.** Removing an MCP server lists the accounts whose
  connections it revokes. MCP sign-in shows "Preparing sign-in" and gives discovery
  and client registration 30 seconds in total. A failed identity lookup records its
  reason on the connection.
- **Codex reserve quota is separate from normal capacity.** An empty reserve raises
  no exhaustion alert while the main quota has capacity. The Usage page keeps the
  reserve collapsed until the main quota is exhausted. Codex model variants are
  priced by their family, so a dated `gpt-5-mini` id gets the mini rates.
- **A lock that a dead process holds frees in 30 seconds.** Provider and OAuth token
  refresh, the sandbox rotation gate, and the browser writer lock renew one TTL. A
  run that loses its lock fails with `LockLostError`. Reading a saved browser
  session's status no longer decrypts its profile.
- **The sandbox and browser images boot with the Drukbox base entrypoint.** The
  SSH key, the caller environment, the secrets-proxy CA, and the `gh` git
  credential come from the base. Both images have `build-essential`. The browser
  image installs Google Chrome on arm64 too.
- **`druks create app` writes hyphenated folder and distribution names.** The app
  name and its package keep underscores. The command accepts both spellings.
- **The execution-defaults error names the agent** that a default change breaks.

### Fixed

- **Druks resumes every run after an upgrade.** A run parked at a gate before an
  upgrade stayed pending, because any edit to any workflow changed the DBOS
  application version. The version is now one constant, so DBOS resumes parked and
  interrupted runs after every upgrade. A run that the new code cannot replay fails
  at the step that differs, with an error that names it.
- **The 0.6.0 upgrade completes on an installation with older agent calls.** Its
  billing-source check failed on calls from before provider-namespaced models. The
  check now applies to new calls, and older calls stay unbilled.
- **A saved browser profile keeps its login cookies.** The launcher writes the
  session-only cookies at shutdown and adds them back at launch.
- **A templated sandbox boots its template image.** Druks sent the installation
  image with the template, so Drukbox booted the plain image and lost the setup.
- **Six open dashboard tabs no longer block a seventh.** A hidden tab closes its
  live streams and opens them again when shown.
- **Hosted deployments run the current Drukbox janitor**, so expired sandboxes are
  removed.
- **A page `Link` to an `/api/` path loads from the server.** Before, the shell
  handled the click and nothing happened.
- **A bare repository name in a ticket matches only that name.** `%` and `_` no
  longer act as wildcards.
- **Druks installs `dbos` below 3.** DBOS 3 refuses the queues at import.
- **The Software Factory timeline shows one running row** for an active agent call,
  and a work-item row omits the PR cell until a PR exists.

## [0.6.0] — 2026-09-15

### Added

- **Software Factory has its own ticket board.** A project holds tickets, and a
  ticket holds markdown comments. Drag a card to change its status. The ticket page
  saves as you type and selects the repository for the pull request. Select `druks`
  as the tracker, and Ready for Agent starts a build. The build reads and comments
  on its ticket through the installation's own MCP server. Its token belongs to the
  run's account, so an agent comment shows the name of that person.
- **The Activity feed replaces the Events page.** Activity records queued runs,
  requests and their answers, and operator cancels. It also records saved results
  and the facts that apps announce. Search by work label, filter by app, type, and
  date, and load older rows. A row opens the saved result and links to the work. New
  rows wait while you read. An app adds its own rows: `AgentOutput.to_event()` names
  the topic of a saved result, and `Subject.announce()` records a domain fact.
- **A live transcript shows when the agent is thinking.** One "Thinking…" line
  shows the estimated token count until output arrives.
- **An agent call can take its own output contract.** Pass `contract=OutputType`
  when the output fields depend on the input. The agent declaration stays the same.
- **An app can run its own CLI with the settings of an agent.** `Agent.get_config()`
  returns the harness, model, effort, billing, and secret entries of that agent.
- **An app setting can offer live choices.** Declare
  `Annotated[str, Choices(source)]`, and the settings page shows the values of the
  source as a select. `choice_details` labels each value of a `Literal` setting.
  `visible_when` takes one shape, `{field: [values]}`.
- **Druks runs on Python 3.14.** CI tests Python 3.11 and 3.14.

### Changed

- **The dashboard is a command center.** A sidebar lists the work of every app, with
  search and phone navigation. The Dashboard shows the requests for you first, then
  failures, then running work, with exact totals. A request opens the app page that
  `ui.page(..., subject=...)` declares for that subject type. Schedules have a page
  below Usage, where you change a cadence or pause a schedule.
- **Settings are full pages.** Each page has a stable link, and search finds every
  field. Unsaved edits ask you to save, discard, or stay. App settings open from the
  gear next to the app name. Connections has tabs for services, accounts, revoked
  grants, and browser profiles. An account row shows the provider identity and the
  granted scopes of its MCP grant.
- **Execution defaults are shared, and preferences are personal.** Settings → Agents
  holds the execution defaults for every account. Preferences holds your timezone.
  One default account runs unattended work, and every run has an account. The
  upgrade keeps the account that 0.5.0 used for unattended runs. The top-level
  `timezone` in `druks.toml` sets the schedule timezone, `UTC` by default. These
  migrations have no downgrade, so back up the database before you upgrade.
- **Credentials stay out of the sandbox.** A sandbox holds a placeholder for each
  credential, and the Drukbox secrets proxy puts the value into each request. This
  covers API keys, Claude and Codex subscriptions, GitHub tokens, and MCP
  credentials. Druks keeps every secret in one encrypted vault and gives a token
  only to a live sandbox. A Drukbox on another server fetches its secrets from
  `[sandbox].issuer_url`. To upgrade, follow "The secrets exchange and the secrets
  proxy" in `docs/deployment.md`.
- **One directory holds the harness configuration.** `DRUKS_HARNESS_CONFIG_ROOT`
  replaces the separate Claude and Codex configuration directories. The default is
  `~/.config/druks/harnesses`, with `claude/` and `codex/` in it. Copy your files
  there, then run the installer again.
- **An agent id includes the app name.** The id of an app's agent is
  `<app>.<attribute>`, so two apps can use the same attribute name. A migration
  moves the overrides of the bundled apps. For any other app, set the agent
  overrides again in the settings of that app.
- **Reviews are part of Software Factory.** The review app is gone, and a migration
  moves its runs, events, and settings. `POST /api/software_factory/reviews` starts
  a review, and its MCP tool is `software_factory_review`. The Dashboard shows
  running and failed reviews in place of the review page.
- **Software Factory works more closely with pull requests.** It fills the pull
  request template of the repository and replaces only its plan block on each
  revision. A mention of the review handle in an inline review comment or a thread
  reply requests a review. An existing GitHub App must turn on the Pull request
  review comments event. A review gets the GitHub MCP server under the review
  identity. The reviewer can post its verdict and inline comments with it.
- **Software Factory reads statuses and assignees from the tracker.** Linear and
  Jira share five status settings: trigger, in progress, in review, done, and
  resting. Each setting lists the statuses of the connected tracker. A status that
  you set in 0.5.0 does not carry over, so select it again. A build runs as the
  Druks account whose tracker connection is the assignee. This works when the
  tracker hides the assignee email.
- **`RepoWorkspace` clones the repository.** A workflow on a repository declares
  `workspace_class = RepoWorkspace`, and Druks clones the subject's repository
  before every agent call. Override `get_repo()` when the subject keeps the
  repository in another field.
- **A database read needs a bound session.** `db_session()` raises outside a
  request, a step, or another platform-bound session. It no longer opens a session
  that nothing closes. In a workflow body, read inside a `@step`. `dispatch()` binds
  its own session. A test request starts with no session, as in production.
- **`AgentOutput.get_artifact()` is now `to_artifact()`.** Rename your override.
  Druks does not call a method with the old name.
- **Usage and subscriptions stay current.** Druks polls each subscription every five
  minutes, also when no Usage page is open. The interval grows to one hour while the
  quota does not change. Manage shows when a subscription last refreshed its token.
  A subscription that the provider revokes stays on the Providers page with the
  reason, the date, and Reconnect.
- **JWT identity uses a JSON Pointer.** `identity.jwt_identity_claim` selects a value
  such as `/traits/email`. The default is `/email`. Before you upgrade, change a bare
  claim name such as `email` to `/email`. A bare name fails validation.

### Removed

- **`App.record_event()`.** Record Activity with `to_event()` on a saved result or
  with `announce()`.
- **`SubjectActivity`, `App.get_subject_activity()`, and `set_run_phase`.** A subject
  page shows the sandbox phase of its run while the sandbox starts.
- **API-key agents on Models.dev providers.** A key for a provider from the
  Models.dev directory still saves, but an agent on that provider refuses to run. No
  proven transport carries its placeholder through the secrets proxy. OpenCode and
  Pi run on Anthropic and OpenAI keys.

### Fixed

- **A service connection that the provider revokes offers Reconnect.** Druks
  disconnects it when a token refresh gets `invalid_grant`. Before, the connection
  stayed live, and each scheduled run failed on it.
- **A step error that DBOS replays keeps its type and message.** Before, some Druks
  errors came back as a `TypeError` or with a doubled message.
- **A run that dies before its cleanup frees its sandbox within one hour**, not at
  the end of the lease.

## [0.5.0] — 2026-09-05

### Added

- **An app page can be pure Python.** `druks.ui` adds pages, blocks, values, fields,
  forms, and operation-backed actions: an app writes `@page` functions and the shell
  renders them with no frontend build. Blocks cover text, markdown, cards, tables,
  lists, facts, metrics, charts, image galleries, timelines, progress, transcripts,
  files, and run controls; fields cover text, number, select, multi-select, radio,
  checkbox, file, multi-file upload, and secret. A page or section can follow a
  subject, or a whole subject type, and the shell replaces just that region when a
  snapshot arrives, so scroll, focus, and half-filled inputs survive. `GateControls`
  reads and answers a parked run's gate from inside the page. A scaffolded app now
  gets a Python landing page and navigation entry by default; a JavaScript frontend
  stays available as the escape hatch for an app that needs full control of its
  screen. The full contract is documented at `docs/druks-ui.md`.
- **A provider can be connected with an API key, not only a subscription.** Pasting a
  key creates the provider and its model catalog on the spot; the Models.dev
  directory is read on demand and cached for a day, only to populate the "add
  provider" search box, not stored and refreshed on a cron for every listed vendor. A
  key is the installation's credential, one per provider, held separately from a
  person's OAuth subscription — an installation can hold both, and each shows its own
  quota, spend, and Replace/Remove or Reconnect controls.
- **Two more coding agents: OpenCode and Pi.** Both ship in the sandbox image
  alongside Claude and Codex, connect through the same API-key or subscription flow,
  and get their own per-harness command, first-byte timing window, and doctor probe.
- **An exe deployment can pull its own sandbox image.** The exe image registry,
  username, and password are configurable settings, and a sandbox template's label
  now reads as `<app>-<script>` instead of a raw file path.

### Changed

- **A provider owns its login, quota, and catalog; a harness only runs on top of
  one.** Settings gains a Providers pane: connect Anthropic or OpenAI with a
  subscription or an API key, and every model id is namespaced as `provider/model`.
  An agent now resolves to a harness, a model, and a billing choice in one place: the
  Agents page lists every agent as it resolves, with an override mark and a lock on a
  billing cell a key-only harness fixes. The separate Harnesses pane is gone; its
  settings moved to the Agents page's defaults and the per-app override cells.

### Fixed

- **An expired login keeps its account email and past expiry on screen and offers
  Reconnect**, instead of looking exactly like one that was never connected.
- **An agent call retries once on an invalid response from the harness**, instead of
  failing the run outright.
- **software_factory's triage step reads the reviewer's own words instead of a stale
  summary**, and each build step gets only the input it needs instead of the whole
  conversation.

## [0.4.0] — 2026-08-29

### Added

- **Files are a platform primitive.** An agent contract declares a `File` field.
  Druks pulls the file out of the sandbox, stores immutable bytes under a stable
  id, and serves it at `/api/files/{id}` behind the identity gate — images, PDF,
  and plain text inline, everything else as a download, validated with a SHA-256
  ETag. An app keeps the reference on its own row with `FileField` and decides
  when to delete it. A file can also arrive as a user upload, not only as an
  agent output. Apps write no transport and no file routes.
- **A workflow declares the sandbox it needs.** `sandbox = Sandbox(setup="sandboxes/build.sh")`
  ships the environment as a plain shell file next to the app. Drukbox builds a
  reusable template from the platform base and that script, identified by content
  hash, and a run that waits for the build shows a sandbox-building phase. App
  authors do not name provider images.
- **Background work runs as a task.** `@task` is the lighter door next to a
  workflow: a durable, replay-recoverable function with no run row, no timeline,
  no subject, and no gates. `every="*/15 * * * *"` runs it on a cadence the code
  owns, `.enqueue(**kwargs)` runs one from a route, a subscriber, or a workflow
  body, and `retries=` sets the retry count for a task or a step. The token and
  model refresh chores are tasks now, so they no longer fill the run timeline or
  ask the operator for a cadence. Apps declare tasks in `tasks.py`.
- **A browser session with no login.** An app can declare an anonymous session, so
  a workflow borrows a real browser for a site that needs no sign-in.
- **Every run shows its transcript.** Each run on the subject timeline has a
  transcript toggle that loads on demand — the newest stays open — so a failure
  further back is readable. A failed run always states a real reason: when the
  crash left no failure record, the run reads the error from its last agent call,
  and a crash before any agent call says exactly that instead of a bare "failed".
- **A repo from a template.** `GitHubClient.create_repo_from_template()` generates
  a private repo under an owner and returns its full name. A name already taken
  returns that repo, so a retry after a partial failure is safe. The GitHub App
  needs `Administration: write` on the target owner.
- **The documentation site.** The guides are published at
  [docs.druks.ai](https://docs.druks.ai), and `docs/` holds the same pages the
  site builds from.

### Changed

- **The pluggable unit is an app, not an extension.** One word runs through the
  whole surface: `from druks.apps import App, AppSettings, Secret`, packages
  register under the `druks.apps` entry-point group and keep the class in
  `<package>/app.py`, the roster and settings routes become `/api/apps` and
  `/api/settings/apps` with `app`, `apps`, and `appSettings` wire fields, and
  `druks create app` scaffolds the package. The ASGI modules move to
  `druks.api.server` and `druks.mcp.server`. One migration renames the events
  column and the settings-override key prefix in place. App slugs, the
  `/api/<name>` and `/app/<name>` mounts, and GitHub App settings are unchanged.
- **The ship app is `software_factory`.** The name now says what the app does:
  a ticket becomes a pull request, planned, built, and gated on the operator
  before it ships. The class, app name, durable kinds, MCP tool, routes, prompt
  paths, and the per-repo config path follow. A data migration rewrites stored
  kinds, event and file app tags, and settings keys, so history and saved
  settings survive the rename.
- **The ORM is async end to end.** Every session on the serving loop is a
  SQLAlchemy `AsyncSession`: an async engine, a request-owned session committed
  at the request boundary, a session per durable step, and async model and read
  surfaces throughout. Sync engines remain only for one-shot processes —
  migrate, the CLI, and the plain doctor checks.
- **DBOS 2.30.** Queue dispatch and partitioned-queue reads are faster, recovery
  is idempotent, and a duplicate `start()` now takes the holder's handle from
  DBOS itself instead of looking the holder up through private API.
- **Workspaces are their own module.** `druks.workspaces` holds the `Workspace`
  base and `RepoWorkspace`. Both sat in the wrong layer: the base resolves OAuth,
  folds the MCP registry, and commits the database — execution policy, not VM
  plumbing — while `RepoWorkspace` was buried inside one app, out of reach of the
  others. `druks.sandbox.datastructures` keeps only its data shapes.
- **Images live under the repo namespace.** Every image this repo publishes is
  `ghcr.io/czpython/druks/<name>` — `sandbox`, `sandbox-sbx`, and `browser`. The
  old owner-level packages stay on GHCR, so an existing deployment keeps pulling
  until it moves.
- **The shared review gate reads app-neutral.** The in-app review component no
  longer speaks in one app's words or claims that a note starts another plan
  pass. It states only what holds for every gate: the note goes to the agent as
  feedback.
- **The webhook host names the addresses it binds.** `DRUKS_WEBHOOK_BIND` lists
  them and defaults to every interface. A host that also terminates TLS with
  `tailscale serve` no longer makes the edge crash-loop on a port 443 bind error.

### Removed

- **The packaged Linear MCP entry.** The catalog ships empty. An operator adds
  the servers they want through the API, and MCP delivery no longer fails on a
  packaged entry that holds no token.

### Fixed

- **The sbx sandbox provider works on a fresh deployment.** The sbx settings and
  auth stores are mounted writable and created with the right owner before the
  first compose command, so the CLI no longer panics on start and a credential
  read no longer fails on a read-only store. The agent toolchain is published as
  an sbx template that boots with no environment, so the first clone in a microVM
  finds `git` instead of dying on `git: not found`.

## [0.3.0] — 2026-08-22

### Added

- **Logged-in browser sessions.** An extension declares the browser session a
  site needs, and the operator signs in once through a Druks-hosted login window
  — real Chrome over noVNC, opened on the site itself, with an optional timezone
  and an egress proxy (authenticated if needed) for sign-ins that reject
  datacenter IPs. A workflow then borrows that authenticated browser; the login
  is encrypted, stored the first time a run reaches for it, and reused across
  runs, and a session set to persist saves its refreshed state after each borrow.
- **OAuth connections.** You can sign in to an OAuth service more than once, and
  Druks keeps each account as its own connection — one per mailbox, handle, or
  workspace, owned by the account that consented and labeled with the provider's
  own facts. A Connections page in Settings lists them; disconnecting keeps the
  row as a revoked, auditable record rather than deleting it, and a repeat
  sign-in reuses the matching connection instead of duplicating it. Extensions
  build on one engine, `OauthClient` in `druks.services`, which runs the
  authorization-code-with-PKCE flow with token caching and refresh; MCP sign-ins
  ride the same engine.
- **Installed apps in the shell.** Every installed app appears in the dashboard
  navigation and renders inside the shell instead of a separate page. An app with
  a frontend ships an ESM `entry.js` and declared tabs and borrows the shell's
  React, components, markdown renderer, and gate controls; an app with no
  frontend still gets pages for free — a home page that stacks its subject
  boards, and a subject page with summary facts, the run timeline, the latest
  transcript, and gate controls.
- **Scheduled workflows can dispatch.** A workflow on a cron cadence can declare
  a `dispatch()`; the schedule fires it instead of `run()`, so a subject-backed
  workflow resolves its subject and starts the real run without a shim.

### Changed

- **Deployment is one profiled compose file.** Every service lives in a single
  `compose.yaml`, with a `hosted` profile for the edge and janitor and a
  `gateway` profile for the SSH gateway, so provider choice is a matter of
  profiles and `.env`. `SERVICE_TOKENS` renders on every shape with no compose
  default, so a missing token stops the sandbox plane instead of it starting on a
  known one, and `install.sh` seeds an empty `compose.override.yaml` it never
  overwrites, so host-local services survive installs and upgrades.
- **A service keys on a derived slug.** A service no longer declares a name or
  title; the slug comes from the class name — `GoogleCalendar` becomes
  `google_calendar` — and the card heading derives from it. The wire field and
  connect route (`/api/services/{slug}`) follow, and an extension that still
  declares `name` or `title` is rejected at load.
- **Board reads know the calling account.** A board scopes its rows to the
  operator reading it, a subject carries a readable label, and a subject page and
  the free board lead with the subject's type and name — `pull request:
  owner/repo#7` — instead of a bare id.
- **One control frame across the shell.** The accent color reaches every surface,
  and the status glyph pulses whenever any run is still going.
- **MCP tool names derive from `operation_id`.** An agent-tagged route gives an
  unprefixed `operation_id`, and Druks prefixes it with the extension name, so an
  author no longer writes the prefix by hand.
- **An app's subjects are checked at load.** A declared subject must supply a real
  read-side; the loader rejects the platform stub and fails the boot instead of
  the first request.
- **An approved, clean PR merges directly** instead of looping back through the
  work gate; only a PR with unmet requirements falls back to auto-merge.

### Removed

- **The per-shape compose files.** `compose.local.yaml` and `compose.remote.yaml`
  fold into the one profiled `compose.yaml`; `install.sh` removes the retired
  overlays on upgrade.

### Fixed

- **Connect forms draw every declared field.** A bool, int, `Literal`, or
  multiline field renders through the shared field component wherever it appears,
  instead of falling back to a free-text box, and the replace placeholder
  reflects the stored secret.
- **A spend-controlled Codex plan reports its quota as a weekly window** instead
  of reading as a parse failure.
- **Sandbox provisioning failures retry.** A transient provisioning error is now
  classified as transient, so the in-run retry covers it.
- **The app engine pool is sized for the run queue**, not a single request.

## [0.2.0] — 2026-08-10

### Added

- **One-command install.** `curl -fsSL https://druks.ai/install.sh | bash` boots
  the local shape: Docker sandboxes by default, no prompts, no TTY. The local
  drukbox runs in compose on the same Postgres, so there is no second datastore.
- **Agent-runbook install.** `INSTALL.md` walks the install as a deterministic
  sequence a coding agent can follow; the README carries the prompt to paste in.
- **GitHub App creation from the dashboard.** The connect card creates the App
  through GitHub's manifest flow and stores its credentials. Installing the App
  is the only step left on GitHub.
- **Service identities.** Connections to outside services live in encrypted
  rows an extension can declare. GitHub and the trackers ride them, and a
  Services settings pane shows each connection as a card.
- **Agents find and start work.** `list_open_subjects` shows an agent what is
  in flight, `ship_start` triggers a ticket, and the surface teaches its own
  contract.
- **Adaptive plan gate.** The planner reports its confidence. A confident plan
  the critic approved implements without parking; anything less waits for the
  operator.
- **Reviews without a review identity.** When no review account is connected,
  reviews post as operator comments instead of failing the run.
- **Sandbox commits carry identity.** Commits are authored by the connected
  App's bot, with the dispatching account as co-author.

### Changed

- **Plans are briefings.** One critic pass, one redraft, outcome-level
  acceptance criteria, one screenful. A parked critique survives an operator
  redraft.
- **One review per revision.** The code-review pass folds into the
  implementation review as a second lens, and one GitHub review carries the
  verdict and the notes.
- **The PR body is written once** and republished on every revision, and it
  states the acceptance criteria.
- **`ship_start` stamps the tracker.** It moves the ticket to the trigger
  status and lets webhook intake open the build. A ticket Druks has never seen
  is a typed 404.
- **Settings panes share one design.** Services, Harnesses, and Skills use one
  type scale, one card surface, one status indicator, one chevron.
- **The agent call page shows the call's artifact.** A plan renders on the call
  page, not only while its run sits parked.
- **Project delete confirms, then cascades.** Deleting a project removes its
  work items after a confirmation that names the scope, instead of a silent 409.
- **`druks doctor` exits non-zero only on a genuine fault.** Pending operator
  setup — connect GitHub, connect a harness — reports as pending and exits 0.

### Fixed

- **Live transcripts parse incrementally** instead of re-parsing the whole
  buffer twice a second.
- **Ticket intake is idempotent.** A re-delivered "Ready for Agent" webhook no
  longer re-builds a merged item.
- **Cancelled builds settle their work items.**
- **Pinned verification commands keep the CI check that proves them.**

## [0.1.0] — 2026-08-02

### Added

- **Multi-tenancy.** Each person signs in through the edge, connects their own
  Claude and Codex subscriptions, and sees their own quota and spend.
- **MCP endpoint.** Druks serves `/mcp`. Point an agent at it with a personal
  access token to answer gates, retry runs, and read usage.
- **MCP server installs.** Search the official registry from Settings and install
  a server by name, connected once for everyone or separately per account.
- **`druks.toml`.** One authored config file; the installer renders `.env` from it.
- **Review extension.** A second bundled app. It reviews a pull request against a
  checkout of the repo and posts one GitHub review.
- **Plan gate.** Choose who approves a plan: a human, the machine, or the machine
  and then a human.
- **Failure recovery.** Agent calls retry transient harness failures and wait out
  quota windows. A run that died can be retried from the step that killed it.
- **Encryption at rest** for harness credentials, MCP headers, and secret settings.
- **Extension testing.** Installing Druks registers pytest fixtures, so an
  extension tests itself without a Druks checkout.

### Changed

- **`build` is now `ship`**, packaged under `druks.contrib.ship` and served at
  `/api/ship`.
- **Trackers belong to ship.** An installation names one tracker and holds its
  credentials in the dashboard.
- **Subjects.** A workflow declares what its runs are about, and the subject
  answers for its own status, timeline, and board.
- **The journal replaces workflow state.** A run records typed facts and
  announces them.
- **Models come from the provider**, not a list shipped in the code.
- **The events feed is one global timeline.**

### Removed

- **The scoper** — no brief agent, no refinement statuses.
- **Environment-variable configuration.** Secrets, endpoints, GitHub App ids, and
  tracker credentials moved to `druks.toml` and dashboard settings.
- **`druks.ticketing`** from the platform. A tracker now only pushes status.
- **`Workflow.set_state()`, `Extension.format_event()`, and
  `Extension.subject_type`**, replaced by the journal, feed facts, and the
  subject class.
- **Jira remote links and sub-task creation.**

## [0.0.1] — 2026-07-12

Initial public release.