Releases: romirom11/ordi
Release list
ordi v1.33.1
ordi v1.33.1
One fix: the agent worker stays online. Every heartbeat after a worker's
first one failed, so the worker went offline in Settings → Agents 35 seconds
after it started, and a standalone worker logged an error every 15 seconds.
The agent worker heartbeat
A worker reports itself every 15 seconds: the first heartbeat inserts its row
in agent_workers, every later one updates it. The row's version column
held the ordi version the worker runs, a string like 1.33.0. The
optimistic-locking trigger from triggers.sql attaches to every column named
version and bumps it by one on UPDATE, and '1.33.0' + 1 is not a thing
Postgres does:
operator does not exist: text + integer
So the insert went through and the update never did. In the api container
that was a warning in the log every 15 seconds and a worker that showed
offline; with the worker split out (v1.33.0) it was a 500 from
/api/v1/agent-worker/heartbeat and agent worker heartbeat failed in the
worker's log on every beat. Runs still executed, because claiming does not
depend on the heartbeat, but the status page could not tell a live worker
from a dead one.
The column is now app_version. Migration 0039_agent_workers_app_version
renames it and drops the trigger a previous start attached to the table;
triggers.sql attaches the bump trigger only to integer version columns
from now on, so the next table with a text one does not repeat this. The
API's response shape is unchanged (workers[].version). A test heartbeats
twice over the HTTP backend and checks the worker is online.
Upgrading
Server-side only: redeploy the image. Migration
0039_agent_workers_app_version runs on start. The desktop app is unchanged
apart from its version number.
ordi v1.32.0
ordi v1.32.0
One writer attaches branches, commits and pull requests to tasks, and the git
webhook finally says why a delivery linked nothing; the release image is
published to GHCR, so a server pulls the app instead of building it; and the
nginx web image, deprecated since v1.6.0, is removed.
Git links you can debug
- One writer. Branches, commits and pull requests attach to tasks through
the forge's webhook – an agent's push exactly as a person's. The agent
worker used to also copy its pull request into a plain task link, which no
page renders; that copy is gone. The webhook's link handling lives in
integrations/git-links.ts; (task, type, ref) is unique ongit_links,
so two deliveries naming the same ref at the same moment leave one row. - One event per transition. GitHub's Redeliver sends a delivery again
under a new id, which the idempotency check lets through; the event it
carries (opened, merged, closed) now fires only when the row's state
actually moves, so a redelivered "opened" does not open the pull request
twice for automations, outgoing webhooks and the stream. - The webhook says why. It always answers 200, because a forge retries
on anything else – so the reason a delivery changed nothing goes to the
log instead: the signature matched no connection, no connection for the
installation (reinstall or resync), the repository is not bound to a
project, no task ref in the branch, commits or pull request, a ref that
does not resolve. A processed delivery logs what it linked, and an error
is logged rather than swallowed. - The URL GitHub must call is on the settings page. The manifest
registers the webhook URL from theAPI_URLof the instance the app was
created from; an app created from a staging host, or before the domain
moved, keeps that URL, and every delivery answers 404 – which ordi never
saw. Settings → Integrations → GitHub now shows the URL this instance
expects, with a copy button, to paste into the app's settings on GitHub
and redeliver.
The image is published
- A server stops building the app. Until now CI built the image and threw
it away: every install ranpnpm installand the web build on the target
machine, and no hosting panel had anything to pull. Every release now
pushesghcr.io/romirom11/ordi– API, workers and the built web app in one
container,linux/amd64– tagged1.32.0,1.32,1,latestand
sha-<commit>. Pin to the exact version and roll back to it; a pre-release
gets only its own tag, and a manual run on a branch pushessha-<commit>
alone, neverlatest. - The tag cannot lie.
/healthzreportspackage.json's version, so a
tag that disagrees with it fails the build instead of publishing a wrong
latest. - A green run means the pull works. The deliverable is not a passing
build: after pushing, the job logs out of the registry, pulls the tag the
way a stranger would, checks the digest is the one it just pushed, boots it
against Postgres and requires/healthzto report this version and the web
app to be served from the same port. If the package is private the
anonymous pull fails with the one-time fix in the log. - The dispatch is deliberate.
release.ymldispatches the image build for
the tag next to the desktop builds. It has to: the tag is pushed with
GITHUB_TOKEN, which fires nopush: tagstrigger, and the release is
published bydesktop.ymlwith the same token, soon: releasewould never
fire either.push: tagsstays for a tag pushed by hand.
linux/amd64 only, deliberately – building this Dockerfile for arm64 under
QEMU takes tens of minutes. If arm64 is ever needed it should be built on
native arm64 runners and the manifests merged, not added to platforms.
The nginx web image is gone
Breaking for deployments that still pin it. docker/Dockerfile.web and
docker/nginx.conf.template are removed. They were deprecated in v1.6.0, when
the API image started serving the SPA, /assets/*, SSE and OAuth discovery
itself – neither compose file has referenced a web service since, so the
image's only remaining purpose was deployments that pinned it.
If you still run ordi-web: point your domain at the api service,
port 3000, path /, then delete the web service and any nginx config
mount you added for it. No new env vars. docs/deployment.md keeps the
pre-v1.6.0 migration step for anyone coming from further back.
CI drops the web image build and the step that booted it to check discovery
through nginx. That coverage is not lost: the same assertion against forwarded
Host/X-Forwarded-Proto already runs against the API image, which is what
serves discovery now.
Docs
The README's Features section sold about two thirds of ordi – no Slack, no
GitHub App, no intake, no CRM analytics or playbooks, no receivables aging, no
team calendar, field groups or questionnaire, no files, reactions or setup
wizard, all of it shipped and all of it on the landing page. The section now
mirrors the site, with every line checked against the API and the schema.
"Migrations run as a separate deploy step" is corrected here and in
architecture-decisions.md: the api container has run them on start since
v1.6.0.
Upgrading
Migration 0038_git_links_unique_ref makes (task, type, ref) unique on
git_links, deleting duplicate rows first – the oldest of each set stays. The
api container runs it on start.
Pull the image and restart:
docker compose pull && docker compose up -d
latest moves you to this release; ORDI_VERSION=1.32.0 pins it. The desktop
app updates itself from the release.
If you still run the ordi-web image, see The nginx web image is gone
above before redeploying – that service no longer has an image to pull.
ordi v1.31.0
ordi v1.31.0
Notifications stop shouting at the whole project and address the person they
concern, the CRM exports a lead with every field it holds, .md attachments
open as documents instead of raw text, the "My leave" card lists your own
absences again, and the MinIO images come from a registry that still serves
them.
Notifications reach the right person
- One recipient per notification. Every notification was derived from a
business event, and every business event streams to a whole project – so
"a task was assigned to you" reached the entire team for a task assigned
to one of them, and the desktop app raised an OS notification for each of
them. Each row is now streamed to its recipient alone as
notification.created, after it has been written, and the web and desktop
clients key off that frame instead of guessing whether a project event was
meant for them. - Who hears about what, corrected along the way: the person who caused an
event is no longer told about their own click; deactivated accounts and
agents are dropped; a comment reaches the task's author, its assignees and
the people who answered on it before, not only the @-mentioned (the
mentioned keep their own notification and do not get the comment twice);
an agent's comments stay quiet, because its run events already report the
same news. - The money events had no recipient at all. A paid invoice, a recorded
payment and an accepted quote notified nobody: the event names the
document, so the invoice or quote now supplies the author, the client's
owner and the ref the email was printing empty. A declined quote is
reported like an accepted one. A merged pull request asks the task for its
people instead of trusting anassigneeIds: []the webhook could not have
filled. A mention on a knowledge page is filed underpage.mentioned, so
it reads as a page mention and opens the page. - Every type can be switched off. Comments, page mentions, merged pull
requests, recorded payments and declined quotes are notifications of their
own now, each with its own switch in email preferences – without one they
would be the only types a person cannot turn off, and the two that used to
ride along with another type would silently stop obeying the switch that
covered them. - The labels and the deep links live in one
lib/notifications, shared by
the bell and the stream instead of a copy each, so a new type can no longer
read astask_assignedin one of them; the link table gains the cases the
bell was missing (a page mention opened the task list, a merged pull
request opened nothing) and is asserted against the table the emails use.
The bell no longer refetches on every event, and the stream stops
reconnecting when the language changes.
Leads export, whole
- Every field, one lead per row. The leads CSV carried eleven of the
thirty-odd fields a lead stores, so the answer to "give me the leads in a
spreadsheet" was to open each record by hand./export/leads.csvnow
builds the whole row: every column of the lead, the related names the table
only hints at (company, contact, owner, labels, next action, converted
deal) and the workspace's own lead custom fields, with select values
rendered as their labels. - It exports what is on screen. An Export button next to the lead
filters, for whoever holdscrm.export, sends the active filters along.
The export is uncapped – one that stopped at the table's 200 rows would
look complete and not be. - Excel reads them correctly. Exports open with a UTF-8 BOM, which is
what makes Excel read them as UTF-8 instead of mangling every accent; the
importer skips a leading BOM, so a file saved by a spreadsheet still
imports. - The download goes through
fetchrather than a plain<a href>: a
navigation carries cookies but never theAuthorizationheader, so on the
desktop build a link answered 401 instead of a file.
Markdown files read as documents
- A
.mdattachment opens formatted. Readmes, specs and exports used to
open as raw monospace text in the file preview. AmarkdownToDocconverter
in@ordi/sharedparses a deliberate CommonMark/GFM subset – headings,
fenced code, blockquotes, bullet / ordered / task lists, pipe tables,
horizontal rules and the inline marks the editor itself offers – into a
tiptap document, which the preview renders through the same read-only
renderer as notes and KB pages. - Safe by construction: the markdown becomes a JSON tree, so no raw HTML
ever reaches the page. Links survive only forhttp,httpsandmailto,
and images become links rather than<img>, so opening a preview never
fetches a third-party URL.
Fixes
- "My leave" lists only your own requests.
GET /leave-requestswithout
anemployeeIdreturns everyone's once the caller holdspeople.read–
right for the team calendar, the People page and Resourcing, and exactly
wrong for the self-service card, which was calling the bare endpoint: an HR
user's own profile listed other employees' absences, each with a cancel
button beside it. A?scope=minealongside the existing?scope=approvals
makes every read the card does explicitly about the caller. An account with
no employee record still gets a 403 there, so the card keeps showing its
"not linked" hint. Balances were never affected –/leave-entitlementsis
scoped by employee. - MinIO images pull again. Anonymous pulls of
minio/mcand
minio/minioon Docker Hub now answer 401, so a fresh deploy with the
minioprofile failed atminio/mc:latestwith "pull access denied" and
only a cached image kept existing servers running. Both images come from
quay.io/minio/…now, pinned to releases that ship/usr/bin/mc, which
the container healthcheck relies on.
Site
The landing page was last revised at v1.27 and had nothing on agent employees
(v1.29), the product's biggest addition since. A feature block after the MCP
section explains the flow – assign, clone, headless Claude Code with the ordi
MCP server, pull request, review – and the GitHub card notes that agents open
PRs through the same App. The desktop step mentions browser sign-in and the
download link in the web app's settings. "Timesheet approval" is dropped from
the Time card and the README: time entries have no approval state or route,
only timers, rates, a weekly view, reports and unbilled time.
Upgrading
No migration. Redeploy the API and web images (or the single image); the
desktop app updates itself from the release.
If you run the bundled minio profile, pull the images again after updating
the compose file – they now come from quay.io/minio/minio and
quay.io/minio/mc at pinned releases, and the old minio/*:latest tags are
no longer pullable anonymously. A running MinIO keeps its data: only the
image reference changes.
ordi v1.30.0
ordi v1.30.0
The first release built by ordi's own agents: three tasks from the board
(ORD-24, ORD-26, ORD-29) went from assignment to pull request inside the
deployment, were reviewed and merged. What they shipped – leave balances a
person can see and cannot overdraw, comments that can be edited after the
fact, and money records that start in the workspace currency – plus the
pull-request hygiene the agents themselves learned along the way.
Leave balances people can see
- Remaining days per leave type:
GET /leave-entitlementsreturns
allocated, carried, used, pending and remaining per type for a person
and period. Until HR has accrued the period the type's annual quota is
the allocation, so a profile never reads "0 of 0" just because nobody
ran the accrual yet; a type with no quota, or one that does not draw down
a balance, is unlimited and says so. Your own card withoutpeople.read,
anyone's with it. - Nobody books more than they have: a request longer than what is left
is refused at submit, with pending requests already held back – two
requests that each fit cannot together exceed the quota – and again at
approval, in case the quota was lowered or another request approved in
between. A period accrued while the quota was 0 keeps its cap once the
quota is raised: it reads "0 days left until HR re-accrues", not
"unlimited". The submit-time checks and the insert run under a lock on
the employee, so two simultaneous requests are checked one after the
other. - The leave card shows what is left: remaining-of-allocated per type
with pending days called out, on your own card and – for anyone with
people.read– on every other employee's page, where HR is asked for it.
The request dialog previews the working days a range costs (weekends and
holidays excluded, with the same shared calc the API charges by) and what
it leaves, and blocks an over-quota submit before it is sent. Request
rows show the deducted working days instead of the calendar span. - The one rule lives in
@ordi/shared(remainingBalance,
exceedsEntitlement) so the card, the form and the API agree.
Comments can be edited
- Edit in place: a pencil on any comment you may rewrite – your own as
a project member, anyone's as a project admin, the same rule the API
enforces on delete – swaps the body for the same rich composer the thread
posts with. ⌘↵ saves, Esc discards, and an edited comment carries an
"(edited)" marker with the time in its tooltip. PATCH /comments/:idhardened before exposure: the body is required
and must contain something – a missing key,nullor an empty document
each used to validate and blank the comment. A mention introduced by an
edit notifies once; mentions already in the comment stay quiet. The
rewrite is written to the activity log.
Money starts in the workspace currency
- Record pickers offer USD, EUR and UAH: the five-code list that lived
in four copies is onelib/currency.ts; a record already stored in
another code (an old PLN expense, a GBP company) keeps it in the picker,
so editing never rewrites a currency silently. The workspace base
currency in Settings keeps its wide list. - Every form seeds from the workspace default: "Add income", the
subscription dialog and the new client and new deal dialogs start in the
workspace currency through oneuseDefaultCurrency()hook, instead of a
hardcoded USD – a UAH workspace stops booking in dollars unless someone
noticed the picker.
Agents
- Pull request descriptions follow the repository's template: the
checkout is searched for a template at the paths GitHub uses, the brief
quotes it, and the description is built from the agent's report
(change, verification, risks) with a link back to the task – saying
outright when nothing was verified. The checklist is left unticked. - Commits carry the task key in the subject where the repository's
convention allows it, else as aRefs:trailer, so the webhook links
them to the task; noCo-Authored-Byor model trailers. The title
collapses to one line, so a trailing space on a card no longer lands in
git.
Upgrading
No migration. Redeploy the API and web images (or the single image); the
desktop app updates itself from the release.
ordi v1.29.4
ordi v1.29.4
One change: branches get English names whatever language the task is
written in.
English branch names in any task language
v1.29.3 transliterated a Ukrainian title into the branch slug
(feature/ord-26-dodaty-dostupnu-kilkist-dniv). Now, before cloning, the
worker asks the cheapest model (haiku, one turn, no tools, through the
same Agent SDK, so a subscription token works) for a 3–6 word English slug
from the task title and description: feature/ord-26-show-remaining-leave-days.
It costs a few seconds and a few hundred tokens per new branch. Without an
answer (the model is down, the answer is not a slug) the transliterated name
is used; "Copy branch name" on the task page keeps the transliteration.
A retry whose previous branch never reached origin (a refused push, or a
feature/ord-26- name left by v1.29.2) is named afresh; a branch that
exists on origin, or a kept checkout with unpushed commits, is continued as
before.
Upgrading
Server-side only, no migration: redeploy the API image.
ordi v1.29.3
ordi v1.29.3
One fix from the first agent run on a task titled in Ukrainian.
Branch names for non-Latin titles
The branch slug kept only ASCII letters and digits, so a title written in
Ukrainian (or any Cyrillic, or with accented Latin letters) produced
feature/ord-26-: the key and the number with nothing after the dash. The
slug now transliterates Cyrillic with the Ukrainian official romanization,
strips accents from Latin letters, and when nothing of the title survives
(an emoji or CJK title) the branch is feature/ord-26 without a dangling
dash. The same helper feeds "Copy branch name" on the task page, so people
get readable branches too.
Runs already on a feature/<key>-<n>- branch keep it: follow-ups and Retry
continue on the branch recorded on the run.
A refused push loses nothing and leaks nothing
The first run against a GitHub App installation that had not yet accepted
the write permissions ended with 403 on push, and the platform then
deleted the checkout with 18 minutes of committed work. Now:
- the checkout is kept when the push fails, and the next run for the task
(Retry) continues in it instead of cloning afresh, so the commits go out
once the access is fixed; - the error says what is wrong and what to do: for a GitHub App, an owner
accepts the updated permissions under GitHub → Settings → Applications →
Installed GitHub Apps; for a token, it needs repo write; - git errors no longer include the
http.extraheaderargument, which was
the installation token in base64, in the run log or the task comment; - long lines in the run log wrap instead of pushing the block sideways.
Upgrading
Server-side only, no migration: redeploy the API image. If your GitHub App
was installed before v1.29.0, accept its pending permission request
(contents: write, pull_requests: write) once; until then every agent
push is refused with 403 and the run says so.
ordi v1.29.2
ordi v1.29.2
Everything the first real agent runs turned up, in one release: the runtime
session now survives between runs, work is never thrown away on any early
stop, and the run log, notifications and Retry behave on the task page.
Sessions survive between runs
Resuming never worked: the runtime's home directory, where Claude Code writes
the session transcript, was created per run and deleted with it, so every
follow-up comment and every Retry answered "No conversation found with
session ID". The work directory is now keyed by task –
tasks/<taskId>/checkout for the clone (still deleted after the run) and
tasks/<taskId>/harness for the runtime's home, which stays. The worker
deletes task directories untouched for 30 days.
When the session really is gone (a rebuilt volume, another worker's disk, the
prune), the run no longer fails: it logs "Session … is not on this worker",
starts a fresh session with the full brief, and tells the agent that the
earlier commits are on the branch and to read git log before continuing.
Nothing is lost on an early stop
- Rate limits: a run parked for quota now pushes what it has and resumes
the same session on the same branch when the window resets; before, it
started from scratch with an empty checkout. - Worker lost mid-run: the session id and the branch are written to the
run as soon as they are known, so a re-queued run continues instead of
starting over. - Any error while finalizing (a database blip, the agent user gone) pushes
the work before the run is marked failed. - Long runs: the push and the pull request use a freshly minted GitHub
installation token; the one from the clone expires after an hour. - A failing
rev-list(renamed default branch, shallow history) no longer
reads as "nothing to push".
What goes into the commit
The worker used to git add -A. It now stages tracked edits and new files
that pass a junk filter: no node_modules/, dist/, build/, coverage/,
.env*, logs or caches, and never more than 500 untracked files at once.
Runs and tasks
- One active run per task and agent: two agents on one task each get
their own run (migration0037_agent_runs_task_agent). - Follow-ups and Retry carry the branch, so renaming the task between runs
no longer opens a second branch and pull request. - A comment written while the run was still queued is picked up when the run
ends instead of being dropped. - A task in a done or cancelled status gets no run, and a task closed while
the agent works is left closed. - Comments by other agents never trigger a run.
- An agent's own run token cannot cancel or retry runs.
The runtime
- The repository's
CLAUDE.mdand.claude/settings.jsonare loaded
(settingSources: ['project']), so the agent follows the project's own
conventions and checks. - The
ordiMCP tools are in the prompt from turn one instead of behind
tool search. BashOutput,KillShellandSkillare allowed, so background commands
are usable.- The worker heartbeat reports the runtime as available only when the
platform binary actually resolves.
The task page
- The run log shows the last 500 events, not the first, so the result
and the error of a long run are visible; streamed events are appended to
the log instead of refetching it, and the run rows refresh every 5 s while
a run is active even without SSE. - Retry is offered on the newest run only and is disabled while a run is
active, with a translated message instead of a raw 422. - Agent notifications are translated and link to the task or to Settings →
Agents. - The agent profile form accepts up to 2000 turns and explains what a turn
is; the budget field says it applies to API-key credentials only; adding
a credential defaults to "Not in rotation" when a primary exists; a
version conflict on Save reloads the row. - Runs that failed before their first turn no longer show "0 turns · $0.00",
and a pending cancel is no longer painted as an error.
Upgrading
Server-side only: redeploy the API image. Migration
0037_agent_runs_task_agent replaces the one-active-run-per-task index with
one per task and agent. Runs that failed with "No conversation found" before
the upgrade can be retried from the task page: their session is gone, so the
retry starts fresh on the task's branch. If several containers claim runs,
mount the same agent_work volume on all of them.
ordi v1.29.1
ordi v1.29.1
A fix from the first real agent task: the run reached its step limit before
the agent could report, and the platform threw its work away. Nothing an
agent commits is lost any more, and the default limit is one a real task
fits into.
Unfinished work survives
A run that hit max_turns, ran past max_run_minutes or was cancelled used
to post a bare error comment and delete the checkout, so the next attempt
started from an empty branch. Now the worker pushes whatever the agent
committed to the task branch – loose changes go in as a WIP commit – and
records the branch on the run, without opening a pull request. The comment
on the task says where the work is and which limit stopped it: "Max turns"
in the agent profile for the step limit, "Max budget" for the spend cap.
Retry continues instead of restarting: it resumes the same Claude session
on the same branch, and the brief tells the agent to check git log and
git status first and finish with as few steps as possible.
200 turns by default
A turn is one model response, which may carry several tool calls, so 60
turns was too few for a task that starts by reading a code base. The default
max_turns moves to 200 and profiles still on 60 follow it; the profile
field accepts up to 2000.
Upgrading
Server-side only: redeploy the API image. Migration 0036_agent_max_turns
changes the column default and moves existing profiles from 60 to 200.
ordi v1.29.0
ordi v1.29.0
The agents release: an AI agent joins the team like any other member, takes
a task and drives it to a pull request inside your own deployment. Around
it, the people side gets the hygiene a real team needs – deactivating
someone hands their open work over instead of orphaning it, a deactivated
assignee can finally be taken off a task, and a task filed under the wrong
project moves instead of being retyped.
AI agent employees
- An agent is a member: the "Add member" dialog gains a switch, "This is
an AI agent" – it creates ausersrow withactor_type = 'agent', no
password and no invite email, plus an agent profile: runtime (claude_code
now;codexis listed as coming soon), free-text instructions, the status
category a finished run lands in (in_reviewby default),max_run_minutes,
max_turns, an optionalmax_budget_usd, per-agentconcurrency, and an
assign policy that says who may hand it work. Agents can be disabled and
re-enabled; avatars carry an agent badge everywhere a person is shown. - Assigning a task is the trigger:
task.assignedto an agent queues a
run. A worker inside theapicontainer claims it (FOR UPDATE SKIP LOCKED, so two workers never run the same job), clones the project's
repository into a fresh checkout, launches Claude Code headless through
the Agent SDK with the ordi MCP server and the agent's connectors, and
streams every SDK message, tool call and result to the task page live
(GET /agent-runs/:id/events, SSE). The run ends with a branch pushed, a
pull request opened with the GitHub App installation token, and the task
in review. Humans review and merge; the existingpr_mergedgit rule
closes the task. A comment on the task continues the same Claude session.
Runs can be cancelled and retried from the task page. - Claude is connected once, at workspace level (Settings → Agents,
agents.manage): either a subscription token fromclaude setup-token
(Pro/Max/Team/Enterprise; lives a year, ordi remindsagents.manage
holders 14 days before it expires) or an Anthropic API key. The secret is
AES-GCM encrypted withENCRYPTION_KEYand never returned to the client;
the card shows who connected it and when, since every run spends that
plan. "Verify" makes a minimal request and records the result.
ANTHROPIC_API_KEYin the environment is accepted as a read-only fallback
for API keys, for PaaS setups configured entirely by env. - A connector library with a per-agent allowlist (Settings → Connectors,
integrations.manage): MCP connectors are added once, from a curated
library or by URL, and each agent is granted the ones it may use. The
agent never talks to a connector directly – every call goes through the
ordi gateway (POST /api/v1/mcp-connectors/:slug/mcp), so no upstream
secret reaches the agent process. OAuth connectors are authorised by ordi
itself (discovery, dynamic registration, PKCE) with the callback at
APP_URL/api/v1/mcp-connectors/oauth/callback; tokens refresh mid-run and
a connector whose refresh failed shows asneeds_auth. - Sandboxed by construction: the child process gets a rebuilt environment
withoutDATABASE_URL,ENCRYPTION_KEY,AUTH_SECRET, S3 or SMTP
settings; it authenticates to ordi with a per-run token that is revoked
when the run ends; tool use is governed by allow/deny lists
(docs/architecture-decisions.md§12). The agent never closes a coding
task itself. - RBAC: one new permission domain,
agents.manage(connect Claude,
create agents, limits, assign policy, connector grants, cancelling runs) –
migration0035backfills it into Owner and Admin. Agents run under the
newAgentpreset role (projects.read, projects.write, kb.read) and see
only the projects they are members of; role ∩ token scope applies to every
call as for any user. - GitHub App: the manifest now asks for
contents: writeand
pull_requests: writeso the agent can push its branch and open the PR.
New apps are created with them; an existing installation has to accept the
new permissions (Settings → Integrations shows the state) – until then
reads and webhooks work and the agent's push fails. - API:
/agents(list, create, get, patch, enable, disable, overview),
/agent-credentials(CRUD + verify),/mcp-connectors(library, custom,
test, OAuth start/callback/revoke),/agent-runs(list, get, events,
cancel, retry).
People and tasks
- Deactivation hands work over (ORD-20): "Deactivate" in Settings →
Users opens a dialog listing the person's open tasks and asking who takes
them – a successor from the active members, or "leave unassigned". Open
(backlog / todo / in progress) tasks lose the leaver and gain the
successor; closed tasks keep their history. Each moved task gets a
reassignedactivity row and the successor is notified like any new
assignee.GET /users/:id/open-taskspreviews the hand-off;
POST /users/:id/deactivatetakes an optional{ reassignTo }and refuses
the leaver themself or another deactivated user. Callers without a body
get the unassign behaviour. - A deactivated assignee can be taken off a task (ORD-22): the assignee
picker still offers active people only, but anyone already on the task
stays in the list, marked "deactivated" and muted, so they can be
un-ticked. Before this the task was stuck with an assignee nobody could
remove. - Move to project (ORD-23): the task row's context menu gains "Move to
project" next to "Delete", with a confirm dialog and a toast naming the
new reference. The move follows the agreed rules: a new number in the
target (the old link redirects), the status of the same name – else the
first of the same category, else the default – for the task and each
sub-task, custom fields keep only the keys the target project or the
workspace defines, labels (workspace-wide) stay, type / cycle / milestone
clear. Sub-tasks, comments, relations, attachments, git links and logged
time travel as before. Moving now needs admin rights on both projects. - Quick-create picks the open project (ORD-21): on any
/projects/:id
route the composer starts with that project instead of the last one used;
a project picked by hand in the same session of the dialog sticks. - Expense currency is a list (ORD-24): the "New expense" form's currency
is the same select the income form uses, starting at the workspace
default instead of a free-text "USD".
Operations
AGENT_WORKER_ENABLED(default1),AGENT_WORKER_CONCURRENCY(2),
AGENT_WORK_DIR(/data/agent-work) and the optionalANTHROPIC_API_KEY
fallback – seedocs/deployment.md§3b. Both compose files mount a named
agent_workvolume: one fresh clone per run, deleted when the run ends,
so size it for concurrency × repository size.docker-compose.prod.ymlcarries a commented-outagent-workerservice
for running agents in their own container (same image, same
DATABASE_URL/ENCRYPTION_KEY/APP_URL, no published port) with
AGENT_WORKER_ENABLED=0onapi. Off by default sodocker compose up
still gives a working system out of the box.- Workers heartbeat to
agent_workersevery 15 s; a run whose worker
vanished is re-queued. Runbook for stuck runs,needs_authconnectors,
event-log growth and disk:docs/operations.md§6. - The API image now ships
gitand the Claude Agent SDK; the build fails if
the runtime binary does not resolve.
Migrations
0035_ai_agents– new tablesagent_profiles,agent_credentials,
agent_connectors,agent_runs,agent_run_events,agent_workers,
mcp_connectors,mcp_connector_oauth; backfillsagents.manageinto
the Owner and Admin roles.
ordi v1.28.0
ordi v1.28.0
The one-person release: an account and its HR card stop being two loose
records with two spellings of the same name, agents finally see the
screenshots pasted into task descriptions, and the task surface picks up
three quality-of-life wins – editable labels, in-row status changes and
Slack-style comment reactions.
One name per person
- The HR card is the canonical spelling:
/users/lookupanswers with the
linked employee card's full name (an active card wins over a terminated
one), falling back tousers.nameonly when no card is linked – so member
lists, assignee pickers, @mentions and comments stop showing whatever the
person typed at signup. - Both doors lead into one room: every card write with a linked user
overwritesusers.namewith the card's full name, and renaming yourself in
the profile walks back into the card (first word → first name, the rest →
last name). Migration0034backfills existing linked accounts once. - The pairing creates itself: accepting an invite links the card that was
waiting under that email (the account takes the card's name) or creates a
linked card from the invite; a card created with an email that already
belongs to an account links on the spot. - A broken pair is loud:
GET /people/link-suggestions(people.write)
lists unlinked cards whose email belongs to an account, and the Employees
directory shows them as a banner with a one-click Link action.
Accounts with no card were already covered by the "No profile" filter. - The profile keeps account things only: the questionnaire and leave
self-service moved to the person's own employee page (the field-group
editor there already honoursselfgrants), and the profile links over
via a "My HR card" row.
Agents see embedded images
docToTextrenders an image node as anline in place, and
textToDocturns such a line back into an image node – a screenshot-only
bug report is no longer an empty body over MCP, and an agent rewriting a
description no longer destroys the images in it.- The MCP layer resolves the stored root-relative signed path to an absolute
url the agent can actually fetch (no sign-in needed – PRD §14.5 links) and
strips the origin again on writes, so stored documents stay
domain-independent. The HTTP-served MCP resolves againstAPP_URLvia the
newpublicUrlclient option, since its API base url only exists on
localhost. - Upsert fingerprints compare the stored (relative) form, so re-running a
generation over a body with images is not mistaken for a hand edit.
Tasks
- Labels are editable (
settings.manage, the same bar as creating):
PATCH /labels/:idrenames and recolors, and the picker gains an in-place
editor behind a pencil on each row – name, the eight-colour palette, and
delete (the FK cascade detaches the label from every task). - Status changes from the list: the status icon on a task row is a
picker – click, choose, the row moves. Same optimistic mutation the board
drag uses; the right-click menu kept offering it too. - Comment reactions, Slack-style: one
POST /comments/:id/reactions
toggles the caller's emoji on and off (stored in thereactionscolumn
the schema always had). Chips with counts under each comment – yours
highlighted, the tooltip names who reacted – plus a quick palette behind a
smile button that shows on hover. MCPget_taskreturns the map per
comment.
HR calendar
- Holiday names wrap to two lines before truncating, and a day with events
opens a popover listing the whole day in full: holidays, birthdays (full
names) and absences with their type and approval state. Works on touch,
where the old hover-only tooltip never fired.
Migrations
0034_sync_user_names– data-only: backfillsusers.namefrom linked,
non-deleted employee cards (active over terminated, newest breaks ties).
No schema changes.