Active work
A person with coding agents runs several sessions at once, and a team multiplies that. Two people’s agents can build the same thing on two branches that share no file, so no merge conflict ever shows it. Active work turns the pushes fremforge already receives into a picture of the work in progress, and warns when two pieces of work look alike.
No agent has to cooperate. Active work reads what the forge already sees: branches, pushes, pull requests and the trailers agents already write.
What it records
When a branch is first pushed, fremforge creates an active work item and updates it on every push and pull-request change:
| Field | Comes from |
|---|---|
| Person | The person who pushed, or the pull request’s author. Agents act on behalf of their person, so the person is always the one named. |
| Agent sessions | Claude-Session: (or any <Tool>-Session:) links and Co-Authored-By: trailers in commit messages and the pull-request body. Only the co-author’s name is kept, never the email address. |
| Title and intent | The pull-request title, the opening of its description, the branch name and the commit subjects. |
| Areas | The top-level folders the branch changes, or the module under folders such as apps/, packages/ or services/ (apps/api, packages/db). |
An item is active while it is pushed to within 7 days. It is done when its pull request merges or the branch is deleted, and abandoned when the pull request is closed without merging. Pushes to the default branch, and branches pushed by fremforge’s own bots or Renovate, are not recorded.
How work is compared
Every time an item changes, fremforge compares it with the other active items in the same repository. Two items are reported as possibly the same work when their titles share most of their meaningful words (for example “Rate limit the login endpoint” and “Add rate limiting to the login API”), or when their wider text overlaps strongly and they touch the same area.
The comparison is text matching in fremforge’s own database. No AI model is used, and no code or text leaves fremforge, unless an org owner also switches on semantic matching on your own model, which is off by default. Words every change uses (“fix”, “update”, “add”) and words that are common across the repository’s own work are ignored, so two changes are not matched just because they are both about the same product.
Across repositories
Work is also compared with active work in the organisation’s other repositories, by its title and intent only (an area such as apps/api in one repository says nothing about another). The same rules for pairs that are not news apply. A match across repositories:
- is shown only to people who can read both repositories: on the Active work page, in the mail, on the answer page and in the read tools;
- never gets a pull request comment and is not sent on the event feed, because the comment is visible to everyone who can read one repository;
- names the other work as
org/repo#12, ororg/repo:branchfor a branch without a pull request.
Semantic matching on your own model
Text matching finds work that says the same thing in the same words. Semantic matching also asks your organisation’s own AI provider whether two pieces of work in progress are the same work when their words differ (“SSO button on the sign-in page” and “Log in with the company account”). It is optional and off by default, and it only ever adds to text matching:
- it can add a match the text did not find, or raise the score of one it did. It never lowers or clears a match;
- it compares work in the same repository only, never across repositories;
- if a call fails, is slow or would go over your limits, matching simply continues with text alone.
Your own provider, and nothing else
Semantic matching uses an AI provider your organisation configured under AI integrations, with your own key and your own contract with that vendor. fremforge holds no key of its own for it and has no fallback: without your provider there is no semantic matching. The vendor is your processor, not fremverk’s sub-processor (DPA Annex B §B.8).
Calls leave fremforge only through a restricted outbound proxy, and only to the host of your provider as it was checked and pinned when you saved the provider. If the provider’s address changes to another host, semantic matching switches itself off until an owner switches it on again, and that is recorded in the audit log.
What is sent
When a piece of work, or the other work it is compared with, has changed (and at most once every 10 minutes for each piece of work), fremforge sends your provider, for that piece of work and for at most 15 other pieces of work in progress in the same repository:
| Sent | Not sent |
|---|---|
The title, without a type(scope): prefix | Code, diffs and file contents |
| The opening of the pull-request description (up to 600 characters) | File paths below the top-level folder |
The branch name, without a leading <login>/ | Commit subjects, pull-request numbers, agent-session links |
| The top-level folders the branch changes | Anyone’s login, name or email address, apart from what people wrote in a title or description themselves |
The other pieces of work are labelled c1, c2, … in the request, never by id or person.
A field with a credential in it is left out. Before anything is sent, every field is scanned with push protection’s own rules: gitleaks’ default rules for the version fremforge runs, plus fremforge’s own (frem.sh tokens, EU cloud providers, connection strings, passwords in configuration). A field that matches is dropped from the request, not masked. If the scan cannot run, nothing is sent at all.
What is kept
fremforge does not keep the request or the provider’s answer. For each pair of pieces of work it keeps only whether the answer counted as the same work and a confidence score, and a one-way hash of the request so the same request is not sent twice. These are deleted with the work and its matches: 90 days after the work finished (its pull request merged or closed, or its branch deleted), or 30 days after Active work is switched off for the organisation. Each call is recorded like every AI feature’s call (feature, provider, token counts, latency), never its content.
Shadow first, then notifications
Semantic matching starts in shadow mode: verdicts are recorded and measured, and nobody is told about them. Thresholds are set from those verdicts before matches it finds are shown. Once it notifies, a pull-request comment or mail about a match that semantic matching found or confirmed says so and names the AI vendor and model that answered.
Limits
- At most one call per piece of work every 10 minutes, and only when what would be sent has changed.
- At most 30 calls per organisation in 10 minutes and 500 a day, on top of your organisation’s AI spending limit.
- Five failed calls in a row pause semantic matching for the organisation for 15 minutes (audit action
active_work.semantic.paused).
Turn on semantic matching
An org owner switches it on under Org admin → AI integrations → Active work: semantic matching. The switch refuses until:
- Active work is on for the organisation (Turn it on);
- the organisation has an AI provider of its own;
- that provider has a checked, pinned host (save the provider again if it was added before 26 October 2026 and the switch says so).
Switching it on or off is recorded in the audit log as active_work.semantic.enabled or active_work.semantic.disabled, with the person, the provider and the pinned host. Test this feature is not offered for semantic matching: it only ever sends the text of work in progress. It is switched on in the console only; the REST API does not configure it.
Mark your contracts
Some files are promises other work builds on: an API specification, database migrations, shared types. Mark them in .fremforge/contracts on the repository’s default branch, one pattern per line:
# API and schema contracts
apps/api/openapi.yaml
packages/db/migrations/**
proto/**/*.proto* matches within one folder, ** across folders, and a line ending in / covers everything below it. # starts a comment. Without the file nothing is treated as a contract.
When a merge to the default branch changes a contract file, every piece of active work in the same area that started before the change gets a Contract changed section in its Active work comment, naming the file and the merge. what_changed_since and list_active_work show it too. Nothing is blocked: it is a reminder to rebase and re-check. The record of who merged the change is kept for 90 days.
Real conflicts: a trial merge
Two branches that share no idea can still collide in the same files. When a piece of work changes, fremforge trial-merges it with the other open work that touches the same area (or that looks like the same work), head against head, inside Forgejo. The trial merge writes nothing into the repository and moves no branch.
When the two heads conflict, both pull requests’ Active work comment lists the other work and the conflicting files, before either is merged. Whoever merges second will have to resolve it, so agree who goes first, or rebase onto the other now. When one side moves and the two merge cleanly again, or one side finishes, the conflict leaves the comment. list_active_work returns the open conflicts too (conflicts).
Each pair is merged once per pair of heads, at most 10 times a minute per repository, and only for repositories under 2 GiB. If the trial merge is unavailable, there is simply no conflict signal; nothing is ever blocked.
Pairs that are not news
Some pairs read like the same work but are not news to anyone. They show on the Active work page with a label, and nobody is mailed or warned on the pull request:
| Label | When |
|---|---|
| Rollback prepared | Both are yours and one is a revert of your change: a revert/ or rollback/ branch, or a title or newest commit that starts with revert(, revert: or Revert ". |
| Same session | Both carry the same agent-session trailer (Claude-Session: or another <Tool>-Session:): one session’s own work. |
| Companion | Both are yours and one names the other: its pull request (#12, org/repo#12 or a link to it, in the description) or its branch name (in the title or the opening of the description). |
| Companion repository | Two repositories, and one of them holds documentation, legal text or runbooks: a repository named docs, documentation, legal or runbooks, a name ending -docs, or any repository with the Forgejo topic docs, documentation, legal or runbooks. The docs for a feature are not a second feature. |
| Possibly related | Two repositories, no folder in common, and a score under 0.85. Across repositories only the words can match, and common words match too easily. |
| Stacked | One branch contains the other’s head commit: it was based on it. |
| Same change | Both branches carry commits that are not on the default branch yet: both were based on the same feature branch. |
Any other pair by two different people is reported, even when one of them names the other’s work: that shows one of you knows, not both. To mark a repository as documentation without renaming it, give it one of those topics under its Settings.
A match is mailed about three minutes after it is found, so that an agent-session trailer that only arrives with the pull request description can still mark the pair as one session’s own work first.
When one change produces several new matches for you at once, you get one mail listing them, each with its Answer link, not one mail per pair.
The comment on the pull request
When a match is found, each pull request involved gets one comment from fremforge-bot:
- It lists the other work: its pull request or branch, the person, the words and areas they share.
- It is edited in place when the matches change and never repeated. When the other work merges or is closed, the comment says there is no overlap any more.
- Text from the other work is shown as plain code, never as links or mentions, and is marked as data. An agent that reads the pull request reads it as information, not as instructions.
The comment never blocks anything. If the work is the same, agree with the other person who continues.
Mail to both people
Both people get one mail per match, in their language, naming the other work, the person, and the words and areas they share, with a link to the other pull request or branch. When both sides are two of your own sessions you get one mail saying so. Each person can turn off Active work mail in their notification settings; an org owner can turn mail off for the whole organisation (email_notify). Nobody is mailed whose Forgejo mail preference is “disabled”.
Answer a match
Either of the two people can say what a match is:
| Answer | Means | What happens |
|---|---|---|
Not the same work (different) | A false alarm. | The match leaves both pull request comments, the page and the read tools at once, and stays dismissed when the work changes later. It counts towards how often Active work is wrong. |
Same work, we’ll join (same) | You join up. Optionally say whose work continues. | Both comments show “Same work, joining” and who continues. |
Split (split) | You split the work. Say who continues the shared part. | Both comments show “Split” and who continues. |
Answer where you meet the match:
- On the pull request: the comment’s Answer link opens the answer page (sign-in required).
- In the mail: the same page, from Answer here. Opening the link changes nothing; only saving the form records an answer.
- On the Active work page: the Answer link next to a possible duplicate, for the two people of the pair.
- With the API:
POST /_app/api/v1/orgs/<org>/repos/<repo>/active-work/matches/<id>/responsewith{"response": "split", "continued_by": "<login>"}(findings:write, read access to the repository).GET …/matches/<id>reads one match and its answer, andlist_active_workreturnsmatch_id,responseandcontinued_byfor each open match.
Only the two people whose work it is can answer, never someone on their behalf: a token that acts for nobody gets no-principal, anyone else not-a-party. An answer can be changed; the last one counts. Every answer is recorded in the audit log as active_work.match.responded, with the previous answer and where it was given. An agent can answer for its person through the API, or through the MCP write tool answer_active_work_match once the person consented to it and the organisation allows connector writes. The answer returns previous_response, the answer it replaced.
Handoff notes
Work changes hands: another person pushes to the branch, opens the pull request for it, or the work simply stops. Active work notices both, and keeps what the first person or their agent knew with the work.
When fremforge notices a handover:
- Changed hands. A push by someone other than the last person who pushed, or a pull request opened by someone other than the last person who pushed. Several sessions of one person are not a handover.
- Idle. An open pull request with no push for 7 days (checked once a day). It is asked about once per idle spell; the next push turns the comment into “work resumed”.
The pull request then gets one handoff comment from fremforge-bot, edited in place, never repeated:
- who handed over to whom, or since when the work is idle;
- Done: the handoff note if one was written, otherwise the commit subjects;
- Next and Open questions from the note (or a
Handoff:line); - the agent session links on the work. A
https://claude.ai/code/session_…link is shown as a link; it opens only for the person whose session it is, so they can resume it there. Everyone else works from the notes.
Everything in the comment is shown as plain code and marked as data, like the overlap comment above. It follows the organisation’s pr_comment setting.
Write the note as a Handoff section in the pull request description (or in a commit message). This is the block to add to your pull request template (.forgejo/PULL_REQUEST_TEMPLATE.md):
## Handoff
Done:
-
Next:
-
Open questions:
- The newest note wins: editing the description replaces it, and a commit message with a Handoff section replaces it too. An empty template is not a note. The note is not compared as intent, so writing one never causes an overlap warning. list_active_work and find_related_work return it under written.handoff (done, next, open_questions, who wrote it and where), and the handover itself as handoff (changed_hands or idle, from, to, at, resumed).
A line for your AGENTS.md or CLAUDE.md:
When you stop working on a pull request, or hand it to someone else, add or update the "## Handoff" section of its description: Done, Next, Open questions. When you continue someone else's pull request, read written.handoff from list_active_work first.Live, on the event feed
Each new match is also an activework.warning event on the event feed, so an agent that already follows the feed learns about it without polling.
Idle work: a daily nudge
Agent sessions end and leave branches and draft pull requests behind. Once a day, fremforge sends each person one mail listing their open pull requests and branches with no push for 14 days, including the work their agents did for them. For each piece of work there are three answers:
| Answer | What happens |
|---|---|
| Continue | No nudge about it for 7, 14 or 30 days. A push resets it anyway. |
| Hand over | Name the next person (they need write access to the repository) and add a note if you like. The work becomes theirs and is in their next digest with your note. On a pull request the note also goes into its handoff comment, edited in place. |
| Close | Closes the pull request (the branch is kept, so it can be reopened) or, for a branch with no pull request, deletes the branch. |
Each item in the mail links to its answer page (/_app/settings/work/idle/<id>). You must be signed in: only the person the work belongs to, or an owner of the organisation, can see or answer it, so a forwarded mail cannot close anyone’s work. Opening the link changes nothing. Org owners also find a Continue, hand over or close link next to idle work on the Active work page. Every answer is in the audit log as active_work.idle.continue, active_work.idle.handover or active_work.idle.close, with the person who gave it.
How rare the mail is:
- at most one mail per person per day, never one per pull request;
- each piece of work is nudged at 14 days without a push, once more 14 days later if nobody answered, and then not again until someone pushes to it;
- a pull request waiting for a requested reviewer is left out: the next step is not yours;
- bots (Renovate and other bot accounts) are never nudged.
Work that already existed before your organisation turned Active work on is picked up too, on the first daily run.
The person can turn the mail off under Idle-work nudges in their notification settings, or with the one-click unsubscribe link in the mail. Each item in a digest is also a work.idle event on the event feed, so the person’s own agents can see it.
Closing idle pull requests automatically
An org owner can turn on auto_close. It is off by default. A pull request is then closed automatically when all of these hold:
- no push for
auto_close_days(at least 60, default 90); - a nudge about it went unanswered for at least a week;
- it has no approval, no requested reviewer and no review asking for changes.
The branch is kept, so anyone can reopen the pull request. The person is told in that day’s digest, and the pull request gets a comment saying why. Branches without a pull request are never deleted automatically.
The Active work page
Org owners see all work in progress at Build & deploy › Active work: one card per repository, work grouped by area, each person’s agent sessions, and every possible duplicate named and linked with its answer. The org switch is on the same page.
Who is working now (presence)
Work shows up here on the first push. Presence shows it earlier: when a person’s AI assistant works in a repository through the MCP server (reading files, pull requests or Active work there), people who can read that repository see that the person is working there now.
- What is shown: logins only. “Working here now: alice, bob” on the repository’s card, a card for repositories where someone is working and nothing is pushed yet, and
working_nowinlist_active_work. - What is kept: one entry per person and repository, overwritten by each call and gone 60 minutes after the person’s latest call. No history, no counts, and no times are shown. Presence is not a measure of anyone’s activity.
- Only a call that worked counts: naming a repository the person cannot read records nothing, and an API token that acts for no person never counts.
- Who sees it: people who can read the repository: the org’s owners and auditors on the Active work page, and anyone with read access through
list_active_work. - Turn it off: an org owner with the Show who is working in a repository now setting on this page (or
presence: falsein the settings API); each person for themselves with Show that I’m working in their notification settings. Either one deletes the presence at once.
Status: available to every organisation. An organisation whose Active work is off, the default, records and shows no presence.
A draft pull request on the first push
Work is compared from its first push, but a branch without a pull request is easy to overlook. With Draft pull request on the first push on, when someone pushes a new branch fremforge opens a draft pull request for it at once, as that person: the work is visible on the forge, in the Active work page and to everyone’s agents before anyone else starts the same thing.
- On by default for organisations created on or after 12 November 2026, off for older ones. An org owner turns it on or off under Build & deploy › Active work (or
auto_draft_prin the settings API). Each person can still turn it off for themselves under Draft pull request on my first push in their notification settings. - The pull request is the person’s. It is opened in their name with a
WIP:title, the first commit subject, and a short description that says how to turn this off. It stays a draft, so it cannot be merged until they removeWIP:and mark it ready. Nothing is ever merged or armed for them. - Its CI waits until it is ready. While the title starts with
WIP:, the draft’spull_requestworkflows do not run, so a draft costs no runner minutes. When the person marks it ready (removesWIP:in the web UI, by editing the title, or through the API), the checks run for the current head commit, so the required checks always exist before it can merge. To run them earlier, anyone with write access presses Run checks on the pull request (or callsPOST /api/v1/repos/{owner}/{repo}/pulls/{index}/fremforge/run-checks): once per head commit, and a second request for the same commit answers409. Push workflows, and fremforge’s own pull-request scans (secrets, licences, security), run as usual. Only the drafts fremforge opens are affected: Forgejo records that when the draft is opened through the one platform account that can open them, and nothing written in a title or description can make or unmake one. A pull request you markWIP:yourself runs CI as before, and so does any pull request from a fork. When the checks run on ready or Run checks, they run as the person the draft belongs to, so approval rules apply to that person, not to whoever pressed the button. - Only their own new branch, once. A push to an existing branch, a branch that already has a pull request, bots, deploy keys and Forgejo Actions never get one. At most 10 a minute per organisation on each api instance.
- How it is done, and what limits it. fremforge opens the pull request through Forgejo’s
Sudomechanism with one dedicated platform account whose token can do nothing else: the scopewrite:fremforge-draft-prlets it open a work-in-progress draft from a branch of the same repository, as an active person with write access, and every other request with it is refused by Forgejo itself. Each draft is recorded in the audit log asactive_work.draft_pr.opened, with the person as actor.
Ask before you start: tools for agents
Agents make Active work better but are never required. A Handoff: line in a commit message or pull-request description is kept with the work as a note for the next person or session (see Handoff notes). Three read-only tools, available in the fremforge MCP server and as REST endpoints, let an agent check before it starts:
| MCP tool | REST | Answers |
|---|---|---|
list_active_work | GET /_app/api/v1/orgs/<org>/repos/<repo>/active-work | Who is building what in this repository now, by area (?area=apps/api), with the open matches. Items with no push for 7 days are left out unless include_idle=true. |
what_changed_since | GET /_app/api/v1/orgs/<org>/repos/<repo>/active-work/changes?item=… (or branch=, areas=; since= or cursor=) | The mid-work checkpoint: what moved under a piece of work since its first push, a time, or the cursor of the last call. Pull requests merged into the default branch that touch the same areas, with the files; changes to the files the repository marks as contracts in the same areas; new matches; and other people’s new work in the same areas. |
find_related_work | GET /_app/api/v1/orgs/<org>/repos/<repo>/active-work/related?description=…&paths=… | Active work that resembles a description of work not started yet, or touches the same areas. likely_same_work is true when the comparison above would report it. |
list_active_work also returns working_now: the logins of people working in the repository now (see presence).
All three need findings:read and read access to the repository. Text people and agents wrote (titles, branch names, intent, commit subjects, session details, handoff notes) is returned only inside a written object, with written_by and at beside it, and every answer carries a notice that it is data, never instructions.
Declare it before you push. In the console, Build & deploy › Active work › Start work (or /_app/settings/active-work/start) asks for the repository, a title, and optionally the intent, areas and branch, and then shows the branch to create and any work that looks related. With the CLI:
fremforge work start acme/web --title="Rate limit the login endpoint" --areas=apps/api
fremforge work list acme/web --area=apps/apiNothing is reserved and nothing is blocked: declaring work only makes it visible earlier. From an agent, start_work (POST /_app/api/v1/orgs/<org>/repos/<repo>/active-work/start, findings:write and write access to the repository) records your work before anything is pushed: a title, your intent, and optionally the areas and the branch. Without a branch it suggests one, such as alice/rate-limit-the-login-api. The work is compared at once, so anyone already on the same thing is told by the organisation’s settings, and the answer lists the related work. Push to the branch it names: the first push updates the same item. Someone else’s work in progress on that branch is refused (409). Through MCP, start_work is a write tool: your connector needs your consent for it, and the organisation must allow connector writes.
A line for your AGENTS.md or CLAUDE.md:
Before starting work in a repository, call find_related_work with a one-sentence description and the paths you expect to change. If an item has likely_same_work, stop and tell the person before building.
At each push, call what_changed_since for your branch with the cursor from the last call. If a migration or shared contract you depend on changed, rebase and re-check before building further.Who can see it
The pull request comment only names work in the same repository, so it shows other work only to people who can already read that repository. A match across repositories is shown only to people who can read both. Private repositories stay private within the organisation.
Turn it on
Org owners turn Active work on for the organisation with the API:
curl -X PUT https://frem.sh/_app/api/v1/orgs/<org>/active-work/settings \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'| Setting | Default | Meaning |
|---|---|---|
enabled | false | Record and compare work in progress. |
pr_comment | true | Comment on the pull requests involved in a match. |
email_notify | true | Mail both people about a match. |
idle_nudge | true | Send each person a daily digest of their idle work. |
auto_close | false | Close idle pull requests automatically, after an unanswered nudge and never with an approval or an open review. |
auto_close_days | 90 | How long a pull request must have had no push before auto_close closes it: 60 to 365. |
presence | true | Show who is working in a repository now (presence). |
auto_draft_pr | false | Open a draft pull request on the first push of a new branch, as the person who pushed. |
Reading the settings needs policy:read, changing them policy:write. Every change is recorded in the audit log as active_work.settings.updated, with the person who made it.
Your own switches
Each person has three switches of their own, in their notification settings or over the API:
| Setting | Default | Meaning |
|---|---|---|
presence | true | Account-wide: others see that you are working in a repository now. Off deletes it at once. |
orgs.<org>.mail | true | Active work mail in that organisation. |
orgs.<org>.auto_draft | true | A draft pull request on your first push, where the organisation has it on. |
curl https://frem.sh/_app/api/v1/users/me/settings/active-work \
-H "Authorization: Bearer $FREMFORGE_TOKEN"
curl -X PUT https://frem.sh/_app/api/v1/users/me/settings/active-work \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": false, "orgs": {"acme": {"mail": false}}}'These are personal, so they need a credential that carries your identity: the token from fremforge auth login, your bench, or an AI connector (read only, as the get_my_settings tool). An organisation API token is refused with user-identity-required. Reading needs orgs:read, changing orgs:write. Only organisations you are a member of now are listed or can be changed (names match without regard to case), and an AI connector does not see organisations whose owners blocked AI connectors. Each change is recorded in that organisation’s audit log as notifications.preference.self_updated, with you as the actor.
Read every setting at once
GET /_app/api/v1/orgs/<org>/settings (orgs:read), also the read-only MCP tool get_org_settings, returns the organisation’s flow-control and AI settings in one answer. What it shows depends on who asks:
- Members see what applies to their own work: Active work’s switches, which branches hold merges (only in repositories they can read), and whether AI connectors may read or act.
- Owners and auditors also see the release-train, risk-tier and runaway-CI settings, the full Active work settings, and each AI feature with its model and the provider’s name and vendor.
It never returns a provider key, key fingerprint, endpoint address, prompt addition or token. There is no write version, over the API or in MCP.
Measuring it
GET /_app/api/v1/orgs/<org>/active-work/metrics?days=30 (policy:read) returns counts and durations only, never the text of anyone’s work:
matchesandmatches_both_notified;notify_seconds_p50andnotify_seconds_p90: the time from the second person’s first push to both people being shown the match (the target is under 5 minutes);false_positive_rate: the share of answered matches marked “different”;dismissed_share_of_notified: matches answered “different” out of every match both people were shown, answered or not (the target is under 30%), andresponse_rate;responses_by_kindandresponses_by_via(api,page,pr_comment,mail).
The Active work page shows the same measure for the last 30 days.
See the API reference for the full schemas.