Repository navigation
v2.3.7
BREAKING: repository access is closed by default, and MCP tokens are per person
Read this before upgrading. The full guide is docs/mcp-access.md.
- A repository nobody has a rule for is now visible only to the workspace
owner, its admins and the superadmin. Until now a repository with no team
grant and no access rule was readable by every member (insingle_tenant
through both the REST API and MCP, inmulti_tenantthrough MCP). After the
upgrade members stop seeing such repositories everywhere: the repository
list, projects, issues, dependency runs, chats, search, and all MCP tools.- To give a team its old access in one step:
analyzer access bootstrap --team <team> --visibility code --all-unruled
(without--all-unruledit only lists what is affected). - To restore the old behaviour as a stopgap on a single-tenant install, set
CELMIS_UNRULED_REPO_ACCESS=open. It is ignored, with a warning, in
multi_tenant.GET /api/access/unruledand a banner on the Team pages
say how many repositories are affected.
- To give a team its old access in one step:
- A repository the caller may not read is answered like one that does not
exist. MCP tools no longer returnblocked_repos,access_notice,
hidden_symbol_countor any note naming a hidden repository, and the REST
routes answer 404 where they used to answer 403 for a repository the caller
cannot see./api/access/myand/api/access/ruleslist only what the caller
may read. - MCP tokens are issued by the superadmin, per person, with an explicit
repository list.POST /api/admin/mcp-tokenstakes the person, the
workspace, slugs and/or globs, an expiry (capped by
CELMIS_MCP_TOKEN_MAX_DAYS, default 90) andallow_write(default false).
The token is shown once. The list can be changed (PATCH) or the token revoked
without reissuing; a change applies within 30 seconds. The new page is
Administration, MCP tokens.POST /api/mcp/token(self-service) now answers 403 unless
CELMIS_MCP_SELF_SERVICE=true. When on, the token carries*and is still
limited to what the person can see.- Tokens issued before this release carry no grant and are refused with 403 and
a reason (an expired, forged or unknown token still gets 401).
CELMIS_MCP_LEGACY_TOKENS=acceptaccepts them, read-only, for a
transition; reissue them instead. analyzer mcp issue-tokennow requires--user,--reposand
--workspace(it used to take--subjectand--scopes).- OAuth keeps working, but only for a person who holds an
oauth_grant
(POST /api/admin/mcp-grants); consent refuses without one, every OAuth
token is limited to that grant's repositories, and refresh fails once the
grant is gone. There is no dynamic client registration.
- Every MCP call is audited: who, token id, tool, repositories touched,
result size and a hash of the arguments. Argument values and results are never
stored. Read it atGET /api/admin/mcp-calls(CSV with?format=csv) or on
the Calls tab. Retention isCELMIS_MCP_AUDIT_RETENTION_DAYS(default 180). - Two migrations,
a7c41e9b2d10(mcp_tokens) anda7c41e9b2d11
(mcp_call_log), sit in the same chain as the review-feature revisions.
Rolling back the code needs no downgrade. - Review fixes to the above.
- The in-app agent, the claude-engine review and documentation generation
reach/mcp/with a short-livedinternalgrant (a row inmcp_tokens,
read-only, never listed, bound to the workspace and the acting person).
They had lost their Celmis tools because the verifier refuses a token with
no row; the person's own access is still the ceiling. - The dependency tools (
get_dep_audit,list_dep_findings,audit_delta),
list_issues,update_issue,list_reviews,get_review_runand
list_alertsnow obey a token's repository list even when the person's own
access is wider, and default-deny when there is no list. A run summary is
rebuilt from what the caller may see.export_sbomfor a repo-limited token
must name one listed repository. ask_codeno longer returnsblocked_reposand the model is told only
that "other repositories" exist: no slug a caller cannot read reaches the
prompt, and retrieval uses the token's list.- A team's read grant no longer overrides a research rule written for another
team; a grant is the fallback only when the repo has no rule. A grant with an
unknown permission value grants nothing. - A call refused at the HTTP edge (revoked, expired, unknown, legacy,
wrong-holder token) leaves amcp_call_logrow (status=denied, tool
(refused:<reason>), token id, no arguments), at most once a minute per
token. The queue counts and warns about dropped rows and is flushed at exit;
analyzer mcp issue-tokenandanalyzer access bootstrapwrite audit
actions;args_hashis keyed to the installation. - A grant can be narrowed, not widened: PATCH rejects a repo list on a
self-service token, a later expiry, and switching write on (the signed token
carries them); issue a new token instead. - Smaller: the roster's emails and job error lines are for workspace admins
only, a project none of whose repositories the caller can read is not shown,
get_reviewtreats a run whose repo cannot be named as unknown in every
mode, a dev-profile token cannot call the full profile's read tools by name,
and the token issue response warns when an entry likeacme/shopmatches the
same repository on two providers.
- The in-app agent, the claude-engine review and documentation generation
- Memories and learning answer 404, not 403, for a repository the caller
cannot read. The review features follow the same rule as the rest: a
repository without a rule for the caller is not there. An editor who used to
see a 403 now sees the same answer as for a repository that does not exist.
The role gates stay: memories are for editors and above, productivity, Jira
task context and credentials for owners and admins. - Review features are limited to repositories the caller may read. The issues
backlog and its summary and recheck, memories, learning, productivity
developer tables, review feedback, the PR commands list, chat replies,
the Jira task context and the requirements check all resolve visibility with
the same resolver as MCP. A caller sees nothing of a repository that is closed
to them; a repository that is only named (metadata) is listed by name, never
opened. - New webhook events: every GitHub, GitLab and Bitbucket hook installed
before this release needs "Repair all webhooks". A hook from an earlier
version does not send what the comment commands, the feedback learning, the
productivity history and the index refresh read: on GitHub
issue_comment,pull_request_review_comment,pull_request_review_threadand
push; on GitLabnote_events; on Bitbucketpullrequest:comment_created,
pullrequest:comment_updated,pullrequest:approved,pullrequest:unapproved
andrepo:push. Reviews keep working without the repair, but@celmis
commands, thumbs and thread resolutions are silently ignored and the webhooks
page lists the hook as outdated. The installer subscribes new hooks only. - MCP calls are limited by default: 120 calls per minute per token, 4 running at once and 60 seconds
per call, and a developer-profile call that fans out names at most 40
repositories. A client that bursts, or a slow call, now gets an error where it
used to get an answer. RaiseCELMIS_MCP_RATE_PER_MINUTE,
CELMIS_MCP_MAX_CONCURRENTorCELMIS_MCP_CALL_TIMEOUT_SECONDS(0switches the
rate limit off); the compose file forwards all three. migrate_consumersno longer takesuser_id. It acts as the caller. A
client or script that passeduser_idmust drop it and use a token of a person
who is owner or admin of the workspace.
Added
-
Ask the reviewer a question in a pull request comment, and get the answer in the thread.
Any text after@celmisthat is not a command word (@celmis why is this a race?) is
answered by the model, with the review comment it replies to, the replies under it, the
diff of the file it is anchored on (the whole change's digest for a comment elsewhere) and
the team's memories in front of it. The answer is posted as a reply in the same thread (a
child comment on Bitbucket, a review-thread reply on GitHub and GitLab, a quoting comment
for a plain GitHub comment) and carries the chat marker, so the next push keeps it. The
pull request's text, the thread and the code are fenced as data, and the answer is
cleaned before posting: no markers of ours, no images, no raw HTML, no live @-mentions.
A bare "thanks" is a reaction, not a model call. Spend is booked under its ownpr_chat
surface on the Usage page; a workspace over its hard-stop budget gets one sentence. New
knobsREVIEW_CHAT_TIMEOUT_SECONDS(90),REVIEW_CHAT_MAX_CONTEXT_CHARS(60000) and
REVIEW_CHAT_MAX_REPLY_CHARS(6000); the per-repository "Answer questions" switch was
already in Commands. Providers gainedget_thread. -
@celmis rememberteaches the reviewers from a pull request comment.
@celmis remember: <rule>stores a team memory for the repository,
--orgfor the whole workspace,--dir=src/api(or--diron a comment
written on a line of a file) for one directory. The rule goes through
memories.remember: a name on the trusted list is active at once, anybody
else's waits for approval on the Memories page, and the thread is told which
happened. The command is listed in the comment guide and inhelpnow that
it exists. A GitHub or GitLab commenter is trusted by login as well as by
numeric id. -
An incremental review still checks the earlier issues of the whole pull
request. The backlog check reads the files of the whole PR, not only of the
new commits. -
Incremental review: a push is reviewed by what is new in it. A new setting
review_scope(repo over workspace over built-in, in General):incremental
(the default) reads only the commits added since the last complete, posted
review;fullreads the whole pull request every time. Whatever is doubtful
falls back to the whole PR: a first review, a force-push that dropped the
reviewed commit, a provider that cannot list the commits, an empty or
unreadable increment, a person asking again with a command or the Review
button. A push of only merge commits, or the same head again, is a quiet skip
that leaves the PR's status and counters as they were. Changes pulled in from
the target branch by a merge are not the author's and are not reviewed.
Comments are anchored and hashed against the whole PR, so a finding in a file
the push never touched stays open. Our inline comments on lines the push
removed are resolved as outdated (a thread a person replied in stays open),
a finding already posted within three lines is not posted twice, and the
summary says how many commits it read. Review runs recordscopeand
scope_base_sha; the run page shows a new "scope" stage. Works on GitHub,
GitLab and Bitbucket.
An earlier comment is resolved as outdated only when it was posted at the
commit the push starts from, so its line number is in the same numbering as
the lines the push removed; older comments stay open, and a comment is
compared with a new finding where its code stands now, not where it was
first written. A push of only files nobody reads (lockfiles, generated
files, the repository's ignore globs) ends quietly instead of reading the
whole pull request again. The same finding twice within three lines counts
as one comment on purpose: the fingerprint is rule, file and title, not the
line. -
PR commands:
@celmis start-review,review --forceandhelpwork from a comment.
Comment webhooks (GitHubissue_commentandpull_request_review_comment,
GitLabNote Hook, Bitbucketpullrequest:comment_createdand
comment_updated) are verified exactly like the pull-request webhooks and
bound to the workspace of the registered repository. A comment becomes a
command only when it starts a token with the bot handle (@celmisor the
/celmisalias, fromREVIEW_BOT_HANDLE); fenced code, inline code and
quoted lines are never read, and a command word followed by prose is a
question, not a command.start-reviewresumes a paused pull request and
reviews the latest commit;review --forcereviews everything again and
skips the draft, title and cadence gates (never the size and enabled gates).
Every command is recorded in a ledger (pr_command_events), which makes a
redelivery or an edited comment run once, drives two rate limits (replies
per pull request and commands per person, per hour; a refusal is announced
once per window; refused commands from strangers do not use the pull
request's budget, and forced reviews have their own small one) and feeds a "Comment commands" timeline on the
pull-requests page. Who may command is a setting,command_permission
(anyone with repository access, participants only, or anyone), next to
commands_enabledandchat_enabled; all three inherit repository over
workspace over built-in and have a new "Commands" section in the review
settings. The bot never answers its own comments. Replies carry a chat
marker, so the next push's cleanup does not delete them. The commands guide
in the review summary is now on by default and lists only commands that are
installed. Webhooks installed before this change do not deliver comments:
the repositories page marks them "needs repair" and offers one button
(POST /api/repos/webhooks/repair-outdated, admin only) that re-subscribes
them all.scripts/pr_commands_smoke.pysends a signed synthetic comment to
a running instance. Migrationa9d4f6b8c034. -
Learning from feedback: what the team dismissed is not posted again.
Every verdict a person gives a finding becomes a signal in an append-only
table keyed by the finding's fingerprint, which does not depend on the pull
request or the line: a reply (a thumb,@celmisplus a keyword, or free text
read by one cheap model call), a reaction polled at the next review of the
same pull request, a resolved thread (a weak signal, upgraded when the fix
follows), the verdict on the reviews page, and what the issues ledger sees
(fixed, or merged unfixed). A newlearned_filterstage between the
prefilter and the model's veto compares each finding with the repository's
signals (same fingerprint; near-identical title in the same directory; or an
embedding of 0.90 or more) and leaves out one that two people on two pull
requests dismissed, or an exact repeat after one. Signals fade by a 90-day
half-life, somebody who said "this was right" blocks the hiding, and a
critical orprovenfinding is never hidden. Two review settings, repo over
workspace over built-in, in a new Learning section:learning_suppression
(shadow, the built-in, keeps every finding and reports what it would have
left out;onleaves them out;off) andlearning_excluded_reviewers
(people whose feedback teaches nothing). A correction in a reply also offers
a pending memory./memoriesgains a Learning view (editor and above, only
repositories one can read) with the counts, the most dismissed findings, the
implementation rate and a way to forget a signal; the reviews page says how
many findings feedback hid or would have hid. Rules from history: a
"from history" job on the rules page, and an optional weekly tick
(REVIEW_LEARNING_RULES_SCHEDULE=weekly), propose rules where several pull
requests agree; they arrive pending with origin "learned". Signals and the
comment map leave with their repository and the person on erasure.
Provider adapters gainedlist_comment_reactions. Migrationc1f6b8d0e256. -
Replies and resolved threads reach the learning loop. A reply in the thread
of one of our findings is read as feedback before the comment-command path
sees it, so@celmis dismissor a thumbs-down teaches the reviewer instead of
being answered as a chat question; a question in that thread (with or without
the handle) goes on to the chat. A named command (review,remember) never
does. Who may teach followscommand_permission, checked only after the
thread is known to be one of ours, so a stranger's reply costs no model call
and ordinary conversation costs one lookup. GitHub hooks now also subscribe
topull_request_review_thread(a resolved thread is a weak signal, a
reopened one withdraws it); existing hooks show "needs repair" until the
repositories page's repair button is pressed. GitLab replies are matched by
discussion id. The implementation rate on the Learning view is now the
issues ledger's own (implementation_statsover pull requests merged in the
window) and respects the repositories the reader may see. -
Requirements check: the completed comment lists every acceptance criterion of the Jira task, and
@celmis -v business-logic <task>checks one on demand.
A review that read a task now adds a "Requirements check" section to the
summary comment, one row per acceptance criterion, marked met, partial,
missing, contradicted or unclear, with the place in the diff that shows it.
A finding of the business-logic agent that cites a criterion always wins;
inchecklistmode one short model call judges the rest, infindingsmode
nothing more is spent and the section says the criteria were not checked one
by one, andoffadds nothing. The mode is the inheritable setting
requirements_check_mode(defaultchecklist), set like every review
setting at workspace, install and repository level, with the labels in all
sixteen languages. The description gets a one-line link to the task, and the
pull-requests page shows the task and its checklist above the review
timeline (GET /api/pull-requests/{id}/requirements, readable by whoever
may read the repository). The on-demand command runs only the business-logic
agent, forced on, against a key, a link to the connected Jira site, or, when
the repository setstask_urls_enabled(default off), a Confluence page of
that same site; it answers in the thread, posts nothing to the code and
records nothing on the pull request. Task text stays fenced as untrusted,
links are https-only, and every failure is a plain sentence, never the
provider's own words. The model sees the diff only through the redacting
code_contextchannel, judges the whole pull request also after an
incremental review, and the on-demand command honourstask_project_keys
(a page is refused while a project list is set). No migration. -
Gates and cadence: a PR can be left alone, or paused when pushes pile up.
Four review settings, repo over workspace over built-in, in General:
ignored_title_keywords(a title containing one, in any case, is not reviewed
automatically: WIP, Revert,[skip review]),review_cadence(automatic
reviews every push,auto_pausepauses a PR afterauto_pause_pushespushes
(3) insideauto_pause_window_minutes(15),manualreviews only on
request). A paused PR gets one note saying how to continue, in the repository's
review language, and is resumed from the Resume button on the pull-requests
page (which reviews everything pushed meanwhile); there is a Pause button too.
The push that reaches the limit is itself the first one skipped. A person's
request (comment command, Review button, CLI, MCP) skips the draft, title and
cadence gates;force(--force,forceon the trigger route) also skips the
target-branch rule. OneReviewRequest(trigger, force, scope, resume) now
carries that through the webhook, the poller, the queue and the orchestrator.
New run stagesgate_titleandgate_cadence; the draft gate now runs before
the repository context is built, so a draft costs no context. Migration
e7b2d4f6a812adds the four columns to both policy tables and the pause state
toreview_pull_requests, and gives every PR whose last review was complete a
baseline commit. A state that cannot be read never blocks a review. A pause
note that could not be posted is tried again on the next delivery; a pause the
cadence no longer asks for is forgotten; removing "WIP" from a title brings the
pull request back for review (GitHub title edits, GitLab polling). -
Auto-review shows which target branches it reviews. The "Auto-review PRs"
card on Reviews says under each repository "→ develop" (or "→ all
branches"), where that comes from (repo, workspace or built-in default) and
links to the repository's settings; "Connect auto-review" says per provider
which branches deliveries are reviewed for and how many repositories override
it.GET /api/reposcarriestarget_branchesandtarget_branches_source,
resolved by the same repo > workspace > install resolver the orchestrator uses. -
A compact, read-only MCP profile for coding assistants at
/mcp/dev. Nine
tools (repos,find,outline,read_symbol,refs,grep,map,ask,
howto) that answer in plain text, not JSON:
the whole tool list is under 6,000 characters, every description is at most 200,
and there is no output schema. Every answer starts with
idx: repo branch@sha age fresh|STALE|unknown, so line numbers always say what
revision they refer to; every list has a token budget, a stateless cursor and a
"narrow with" hint.findranks exact, prefix, token, substring and fuzzy
matches, puts tests and vendored copies last, uses how often a symbol is called,
and never lets one repository fill the page.read_symbolprints exactly the
lines of one symbol from the indexed commit, with the middle elided past a
limit;grepreads committed text only, never secret files, and names the
enclosing symbol. The scope isread:code; a token without it is refused at the
transport and again inside each tool. A repository the caller may not read looks
exactly like one that does not exist, and all body text goes through one
redaction hook. -
A push to Bitbucket re-indexes the repository. The hook now subscribes to
repo:push, and a push to a branch goes to the same freshness check the daily
sweep and the GitHub handler use; tags and deleted branches are ignored. -
get_api_surfacereads routes from source. FastAPI/Flask decorators, Express,
Laravel, Symfony and Go registrations are found with onegit grepat the
indexed revision (heuristic, and it says so); a repository with no readable
revision answerssupported: falseinstead of an empty list. -
The index remembers which branch it indexed (
repo_index_state.indexed_branch,
migrationb3e9d27f5a40), and each symbol carries a usage rank written at index
time. -
A Claude Code plugin for the developer MCP profile (
celmis-code). Install
with/plugin marketplace add <repo>then/plugin install celmis-code@celmis.
It wires${CELMIS_URL}/mcp/dev/withAuthorization: Bearer ${CELMIS_TOKEN},
and ships thecelmis-searchskill (search order, howto rules, freshness,
cursors, guardrails), a read-onlycelmis-exploreagent, and three hooks that
are stdlib-only and fail open: a one-time session note on index freshness, a
one-time hint before the first identifier-like Grep/Glob, and a post-call check
that says which files the index sha is stale for (git diff <sha> -- <paths>,
no shell) so the agent reads those locally. There is no local proxy and no
versioninplugin.json, so a plugin update follows each commit. Docs:
packaging/claude-plugin/celmis-code/README.md, including the
headersHelperform (bin/celmis-auth-header, keychain first) and a
managed-settings snippet. -
A local end-to-end harness for the developer profile.
tests/e2e_local/
boots the real app in-process (random port, SQLite, real indexing, real MCP
client over HTTP) on three fixture repositories (Python, TypeScript, Go) whose
secrets are planted at run time only.scripts/dev_mcp_e2e.pyruns 12 gold
scenarios (howto, find, outline, symbols, refs, grep, and three secret probes),
scores them against a deterministic no-Celmis baseline (tokens and calls),
and fails on any leaked value, reporting only labels. Run it with--spawn
or--url. Lane-dependent tests (ACL and token matrix, audit rows, scenarios,
canary scan) skip by feature probe; setCELMIS_E2E_STRICT=1to make a skip a
failure../scripts/e2e.sh mcp_devis a contract smoke against a live stack; it
needs a real issued token (E2E_MCP_TOKEN) and skips without one, because a
self-signed JWT is refused by/mcp/dev/. The runner's report now says whether
the leak scan was armed (--require-leak-scanmakes "not armed" a failure),
--min-scenarios Nfails a run that skipped too much, secrets are scanned in
both the text and the raw JSON, and a name counts as listed only inside a
howto answer'sinputssection. New HTTP tests cover cursors (no gaps, no
duplicates, stale cursor), default token budgets per tool, the fresh and STALE
freshness line around a push, and single_tenant installs. -
A review closes with "Code Review Completed", and the PR hears a greeting first.
The started comment says hello on a PR's first review ("Hi! I'm Celmis.
Starting the review of commitabcdef1(3 files)...") and "New changes -
updating the review" on a later push. The closing comment leads with the
verdict and the counts (found, by severity, by category), lists the top five
findings with their place and a pointer to the inline comments, and ends with
the scope and a link to the repository's review settings. A commands guide
can be added under it. The previous layout stays available asclassic.
All of it speaks the repository'sreview_languagethrough the message
catalog (en,uk). -
Two new review settings, wired through every layer.
completed_comment
(completedby default, orclassic) andcommands_guide_enabled(off by
default) inherit like the other settings: workspace default, repository
override, effective value in the API, a row each in the Summary section of the
settings UI in all 16 locales (Ukrainian translated, the rest carry English
text until translated), migrationd6a1c3e5f701. -
The PR description carries the findings. Between the overview and the
changes walkthrough there is a findings block (total, top findings, how many
were left as inline comments, or that nothing was found). A re-run of the same
commit rewrites the stamp without a clock time, so the description settles
instead of changing on every run. -
ReviewBatch.summary_sections. One list of named, ordered blocks that the
summary comment and the description render from, each marked for the comment,
the description, or both. Later stages add a section instead of editing the
composers. -
Team memories. Short facts the team teaches the reviewers, kept per
workspace, per repository, or per directory of a repository, and told to
every review of the files they concern. One store (review_memories, table
and three settings in migrationf8c3e5a7b923), one module
(src/review/memories.py) for the writing:remember(...)decides who is
trusted (the token owner, a workspace editor or admin, or a name on the
memory_trusted_commenterslist), asks the model once whether the fact is
already known (skip, merge or create; a model that is down stores the fact
rather than losing it), and files a stranger's request as pending. Only
active memories reach a prompt: fenced as<team_memories>data under a
preamble that puts the output contract above them, cut to
REVIEW_MEMORY_PROMPT_CHARS(default 3000) broadest first, and a directory
memory is told only for pull requests that touch it. New/memoriespage
(approve, reject, edit, bulk, search, a budget meter) and/api/memories;
the repository policy's prompt preview shows what a review is told. New
Learning section in the review settings withmemories_enabled,
knowledge_approvalandmemory_trusted_commenters, inherited repository
over workspace like every other setting. A removed repository takes its
memories with it; a GDPR export lists what a person wrote and an erasure
unlinks their name from it. Memories are for workspace leads: viewers and
members get a 403 on every/api/memoriesendpoint (and no tab); an editor
sees and edits a repository's memories only where a team of theirs grants
access, owners and admins see all of their workspace, and a repository one
may not read is never named, counted, previewed (also not in the prompt
preview) or told to a chat answer. -
Issues backlog and auto-resolve. An issue from a merged PR now stays on
the backlog (open, with its target branch) instead of vanishing with the PR,
and Celmis closes it once the target branch no longer has the problem:
after each review of a later PR, on a merge, on a push, from a manual
"recheck" and in a daily sweep. A blob-hash skip and an "anchor still there"
check come first; only a gone anchor goes to the model, which can say
fixed, not fixed or unsure, and only "fixed" closes (a file counts as gone
only when its deletion is confirmed, renames are followed). An issue closed
by the machine reopens when its anchor comes back; a person's decision never
is overruled. The implementation rate (implemented, unimplemented,
dismissed, abandoned) is frozen at merge and later fixes are reported as
resolved_later;/api/issuestakesscope,resolution,outcomeand
include_duplicates, and/summarycarries the backlog figures. Four new
inheritable settings (workspace and repository, all 16 languages):
issues_auto_resolve,issues_resolve_llm_verify,issues_resolve_max_llm
(0-50, default 8, one call per file chunk, own budget surface
issue_resolve) andissues_announce_resolved. New env:
CELMIS_ISSUES_RECHECK_DEBOUNCE_SECONDS,CELMIS_ISSUES_SWEEP_INTERVAL_HOURS
(0 turns the sweep off),CELMIS_DISABLE_ISSUES_SWEEP=1. Migration
b0e5a7c9d145. Review fixes: a repeat whose canonical issue a person
closed (or that was fixed before the repeat merged) joins the backlog
instead of going unchecked; the revert watch covers only the last 60 days of
auto-fixes and never crowds open issues out of a pass; a merge recheck that
found the branch busy asks again; a head read from a possibly stale local
clone no longer decides a merge-time check; finding titles are fenced in the
verification prompt; the manual recheck runs one branch at a time with the
repository owner's provider. Second review: a merge stamps the branch the PR
really landed on (a retargeted stacked PR no longer keeps a deleted branch);
repeats are linked, closed and reopened only within one target branch; a
revert reopens an auto-fixed issue only where the issue was raised, not for
the same line elsewhere in the file; the recheck reads the repository's own
policy (an unbound repository's opt-out holds, an unreadable policy skips
the pass); a pass over more than 500 open issues moves on (least recently
checked first); the model's code region is capped in characters; asking for
one PR's issues no longer hides its repeats. -
The business-logic check reads the Jira task. A pull request that names
a task (PROJ-6066in the title, branch, description or commits, Cyrillic
branch names and keyboard-layout lookalikes included) is now held to the
task's summary, description and numbered acceptance criteria (AC1,AC2, …)
as well as to its own text, and a pull request with an empty description is no
longer skipped when its task can be read. Connect Jira on the Connections page
(site address, Atlassian email, API token; or reuse the Bitbucket token on the
server): the site must be an https Atlassian Cloud address or a host in
JIRA_ALLOWED_HOSTS, resolve to public addresses, and the token is verified
withGET /rest/api/3/myselfbefore anything is saved. Five new inheritable
settings (repository > workspace > install) in Review categories:
task_context_enabled,task_project_keys,task_acceptance_field,
task_include_comments,business_logic_auto(when_task_foundswitches the
agent on for pull requests whose task was read; naming it in
disabled_agentsstill wins). The task text reaches the model fenced as
untrusted evidence, secrets masked, capped, and cached per workspace
(task_context_cache,JIRA_CACHE_TTL_SECONDS); a Jira failure is a sentence
in the skip reason, never a failed review. New findings rule ids
logic.requirement-missing,logic.requirement-partial,
logic.requirement-contradicts. Admins can preview a task as the model reads
it (GET /api/task-context/issue/{key},/fields,/projects). Migration
d2a7c9e1f367; providers gainedfetch_commit_messages.
Who can make the bot read a task: the connection is one workspace token, so
withtask_project_keysempty a key written in a pull request is read from
any project that token can browse, whoever wrote the pull request. Set
task_project_keysto the projects reviews are meant to read. Disconnecting
or replacing the Jira token also drops the stored task reads of the
workspace; a review waits at most 30 seconds in all on Jira; project keys are
letters and digits (no underscore) in settings, the key check and the finder. -
Productivity metrics and the
/productivitypage (Enterprise, under the
analyticslicence feature). Reads the pull request history the sync
backend collects and answers how fast work moves and how stable releases are.
Owners and admins only, like spend and review cost (the routes answer 403 to
everyone else and the tab is not drawn for them):
cycle time split into coding, pickup and review; lead time for changes;
deployment frequency; change failure rate; time to recover; merged pull
requests; pull request size and cycle time by size; and, when the review
issues ledger records outcomes, the share of review suggestions taken. Every
figure is a median with p75/p90 and a comparison against the previous equal
period, with DORA bands for the four delivery figures. Filters by repository,
repository group, author and target branch; a slowest-pull-requests table and a
developer activity table that withholds a median below three merged pull
requests. The page also hosts the data-source panel: switch a repository on,
watch the backfill, start or re-read a sync, estimate the backfill and edit the
workspace and per-repository settings. The routes live under
/api/analytics/productivityand are absent without the licence; capabilities
reports theproductivityfeature accordingly. Labels ship in all sixteen
locales. Cycle time counts only pull requests whose detail has been read (an
unread one can carry an approximate merge time); a target branch that no
deployment lands on no longer zeroes the delivery figures; ignored accounts are
not offered as authors; and the page says when the previous period is older
than the synced history. -
Productivity sync backend (AGPL core). Six tables (
productivity_repo_settings,
productivity_pull_requests,productivity_pr_events,productivity_deployments,
productivity_deployment_prs,productivity_sync_state; migratione3b8d0f2a478) and the
writers that fill them, insrc/productivity/. Opt-in per repository: nothing is read
from a provider untilenabledis set. Settings layer repo row, then workspace row,
then built-in (settings.py).sync.pyruns list, detail, release shas, revert
resolution and deployment rebuild under a soft lease, a 480 s time budget, a
per-credential token bucket (ratelimit.py) and a stored watermark, so a run
that hits a budget or a 429 resumes where it stopped (continuation job
prodsync-next:). Adapters for Bitbucket, GitHub (GraphQL) and GitLab are in
providers/. Bot comments, the author's own comments and quoted lines do not
count as the first review; PRs are classified revert, hotfix, bugfix or
feature (Latin and Cyrillic titles); deployments come from merges into
integration branches, provider deployments or tags, with failures and recovery
time. The review webhook marks a PR stale and a merge refreshes it; approved
and unapproved events are now subscribed. Scheduler env vars:
CELMIS_PRODUCTIVITY_INTERVAL_MINUTES(60, 0 turns it off) and
CELMIS_PRODUCTIVITY_FIRST_DELAY_SECONDS(180). The metrics reader, API and
UI are Enterprise and not part of this change.Review fixes: a backfill's continuation job is keyed by the job that queues it
(prodsync-next:<repo>:<job id>, likewiseprodpr-next:), because the queue
dedups against running rows and a shared key stopped the chain after two slices;
one unreadable release PR no longer blocks the deployment rebuild; Bitbucket
commit lists are walked when the API leaves outsize; Bitbucket's 12-character
PR hashes match the 40-character commit lists; deployments are upserted (stable
ids) instead of deleted and re-inserted; a merge webhook reuses the stored provider
deployments instead of re-reading them; a GitHub list cut short by the page cap is
an error instead of a silently advanced watermark. -
Markers are invisible on Bitbucket. The
<!-- … -->lines Celmis finds
its own comments and the description block by were shown as text in every
Bitbucket comment. One module,src/review/markers.py, now owns every
marker (review,status,summary,chat:v1,finding): the code keeps
writing the HTML form, the Bitbucket provider hides it on the way out
([//]: # (x), or zero-width characters as the fallback) and reveals it on
the way in. A marker counts only as a line of its own, so a quote reply that
copied it is not ours. Old comments and descriptions with HTML markers are
still recognised and are converted by the next write. If Bitbucket renders a
hidden marker anyway (checked on thecontent.htmlof every write) the
process switches to the zero-width form by itself;REVIEW_MARKER_STYLE
(auto|html|refdef|zwsp) pins it.scripts/probe_bitbucket_markdown.py
shows on a test PR which form Bitbucket hides. -
No raw HTML on Bitbucket.
<sub>,<details>/<summary>and&
become_italic_and**bold**at the Bitbucket boundary; fenced and inline
code is never touched. Comments and descriptions are capped (30 000
characters) without cutting a marker, and the overview in the description
gives way before the author's own text does. -
A description whose markers were lost is repaired by its heading. When
someone edits the description in Bitbucket's editor and the hidden lines
disappear, the next write finds our block by## 🤖 Celmis summaryinstead
of stacking a second one. -
Markers are whole lines outside code, whatever the line ends. A marker
quoted mid-sentence, in an inline code span or in a fenced block (a finding
about Celmis' own code) is no longer rewritten into a real hidden marker; a
description saved with Windows line ends is read like any other; a
description over the limit is cut from its longest stretch of text, so the
summary block keeps its start and end markers around its content. -
The bot's own text comes from one catalog, in the review language
(src/review/messages.py, English and Ukrainian, English fallback): the
"reviewing…" placeholder, the skipped/failed notes, the "not reviewed" note
and the lost-diff note.ReviewBatch.review_languagecarries it. -
REVIEW_BOT_HANDLE(default@celmis), the mention PR commands will answer to. -
A guard test walks every inheritable review setting through the resolver,
both policy tables, the four API schemas,api.ts,model.ts, the settings
section and the English texts, so a new setting cannot be wired halfway. -
howto(topic, repo)on the MCP server. Asks how a repository does one of seven
things (db,auth,config,http_client,logging,messaging,cache) and
answers with the code slices that show the pattern, the names of the environment
variables and settings it reads, and where each value comes from (.env.example,
compose, KubernetessecretKeyRef, CI variables, Dockerfile, app config). It never
returns a value, and ends by telling the agent to copy the pattern, take the values
from those sources and ask the user or ops. It answers only for repos the caller may
read as code; a denied repo and a missing one give the same reply. Registered on
/mcp/and stdio under theread:graphscope (carried by every token kind), and exposed asregister_howto(mcp)
for other servers. -
Every MCP tool output goes through one redactor.
redact_for_mcp
(src/security/mcp_redact.py) replaces secrets with[REDACTED:label]in DSNs
(including URL-encoded ones), auth headers,key = valueassignments in code, YAML,
JSON and properties, PEM blocks, provider keys, KubernetesSecretdata and
high-entropy strings. References stay (os.getenv,${X},secretKeyRef, vault
paths,changeme), as do SHAs, UUIDs and integrity hashes. It is always on and
ignoresredaction_enabled.output_guard.install_output_guardapplies it to the
result of every tool and fails closed: if redaction raises, the caller gets
"output withheld: redaction failed" and nothing else. -
Secret files are not indexed and not readable through the tools.
src/security/secret_files.pyclassifies a path asdeny(.env*, private keys,
credential stores,*.tfstate, kubeconfig,kind: Secret),keys_only(names kept,
values masked) orok. The graph walker skips denied files, the file readers and
the exploration agent's grep refuse them with one message that does not say whether
the path exists, andGIT_PATHSPEC_EXCLUDESkeeps them out of git-backed scans.
secret_path_globs_extraadds repo-specific patterns. -
Review fixes for howto and the redactor.
- A committed symlink is never followed:
.envbehind a link, or a file outside the
clone, was readable throughhowtoand indexed by the walker. Symlinks are skipped
by the file lists, the readers, the trace and the walker; the indexer no longer
parses a real.env. The deny list gained.envrc,.env-*,.pgpass,
.my.cnf,.dockercfg,.s3cfg,client_secret*.json,*.keytaband
*.pem.*, and the git pathspecs ignore case. - Redaction no longer takes quadratic time on one long word (20k characters took
12 s and stalled the loop). Runs of 2048+ non-space characters are replaced by
[REDACTED:long-literal], the PEM and PuTTY rules are linear, the guard runs the
redaction off the event loop, and a redaction past its time budget is withheld. - Secret names are split on
_,-,.and camelCase:db_pass,dbPass,PGPASS,
pw,creds,HMAC_KEY,AccountKey,PEPPER,SEEDand{"name": "DB_PASSWORD", "value": ...}pairs are redacted. A quoted 40/64-digit hex under a
neutral name or a key-like name is redacted; SHAs after a hash-like word stay. - New forms: Azure
AccountKeyandSharedAccessKey, Slack, Discord, Teams and
Telegram webhook secrets, pre-signed URL signatures,curl -u,mysql -p,
--password x,CookieandSet-Cookie, anduser:pw@host/dbwithout a scheme. - The detailed
howtoanswer applies the caller's path rules to dependencies and
related tests, slices are cut at 400 characters per line and at the caller's
token budget, and the tool description is under 200 characters.
- A committed symlink is never followed:
Security
Found by the access audit of the dev MCP, and by wiring the review features onto it.
Each item has a regression test.
-
Reading a repository's reviews needs the right to read the repository.
GET /api/reviews/{id},/diff,/findings, the history list and
GET /api/pull-requests/{id}/runsused to check only the workspace. They now
apply the same default-deny as MCP; a closed repository answers 404, exactly
like a run that does not exist. A team access rule belowcode(metadata,
none) now also narrows a team grant on the REST side, as it already did in
MCP; owners and admins are never narrowed.metadatastill lets a repository
be named in a list, never opened. -
Groups, Claude Code sessions and "who works on what". A group only takes
repositories its creator may read (a refused one gets the same 422 as an
unregistered one); cross-repo drift greps only the siblings the reviewer may
read, never.env*, keys or credential files, and masks secrets on the quoted
line;POST /api/agent-sessionsrefuses a repository the caller may not read;
GET /api/repos/developerslists only repositories the caller may see. -
An MCP token dies with its account. A deactivated person is refused on
every call, and erasing a person (GDPR) revokes all their tokens
(mcp_tokens_revokedin the answer). -
migrate_consumersno longer takesuser_id. It acts as the caller,
needs the owner or admin role of the workspace (or a global admin) andcode
access to each repository it changes. Breaking for clients that passed
user_id. -
askhonours the repository list of its token, like every other tool. -
grepis no longer an oracle for redacted values. A hit whose match lies
inside a redacted region is dropped, so a value cannot be recovered one
character at a time by guessing the pattern. -
Redaction now also covers Kubernetes
name:/value:pairs, XML
elements and attribute pairs,.npmrctokens,docker login -p, setter and
define('X_PASSWORD', ...)calls,Pwd=in code strings, DockerfileENV,
htpasswd/crypt hashes, full-width:and=, and "the password is ..." prose. -
Credential files added to the deny list:
*.tfvars.json, PKCS12, GCP and
Firebase key files,wp-config.php,local_settings.py, Ansible vault files,
values-secret*,htpasswd,vault-password.txtand similar names;.env,
.env.and names with zero-width characters no longer slip past. -
howtooutput is not an injection channel. Secret-store names and file
names printed from a repository are validated and shown as inline code. -
The Claude Code plugin refuses plain http.
celmis-auth-headerwill not
print a token for aCELMIS_URLthat is not https (localhost excepted), and
the hooks say so instead of nudging towards the server. -
Per-token limits on the MCP server (against one token starving the rest):
CELMIS_MCP_RATE_PER_MINUTE(default 120,0= off),
CELMIS_MCP_MAX_CONCURRENT(default 4) andCELMIS_MCP_CALL_TIMEOUT_SECONDS
(default 60). A call over a limit is answered as denied and audited. A dev
call fans out to at most 40 repositories. -
Role limits on the new surfaces are one rule, checked everywhere.
Memories are for editors and above (an editor sees only the memories of
repositories their teams may read, plus the workspace-level ones),
Productivity is for owners and admins, and the Jira connection and
/api/task-context/*are for owners and admins; viewers and members get a
403 and no navigation entry. The connection list stays open to every member
(the dashboard needs to know whether a provider is connected) but now tells
only owners and admins which account, host or e-mail is behind it, and no
endpoint returns a token. The Connections page and its tab follow the same
role and say so instead of offering forms that would answer 403. -
A new route cannot skip its gate. A test walks every mounted router and
requires the matching role dependency on each route of the memories,
learning, productivity, task-context and credential surfaces, and fails on a
route that names one of those surfaces without being in its table. Role
matrices (owner, admin, editor, member, viewer, a member of another
workspace, somebody with no workspace, global admin) now run against the
real membership rows for the Jira and credential endpoints and for every
productivity endpoint. -
One access model for every review feature.
code_readable_repo_slugs
(REST) mirrors the MCP resolver: memories, learning, review policies, the
issues backlog (list, summary, recheck, status change), review feedback and
the PR commands list read repositories the caller may open, not merely name.
A test compares it witheffective_accessfor every principal, and a test
walks every route and every MCP tool of both projects. -
Chat replies and the business-logic answer go through the central
redactor (redact_for_mcp) before they are posted; if it fails, the answer
is withheld, not posted. -
Review feedback routes check the run.
GET,PUTandDELETEon
/api/feedback/run/...answer 404 for a run in a repository the caller cannot
read, and look rows up inside the workspace. -
An MCP tool cannot open a role-gated surface the REST API closes (a test
checks the memories, issues, productivity and task-context tools).
Fixed
-
Closing a repository graph no longer waits ~4 s. Every MCP tool call opens
the embedded graph and closes it again; the close sent SHUTDOWN and then let
redis-py's default retry policy reconnect, with backoff, to the server it had
just stopped. Alist_reposover four repositories took ~16 s. The close is
now one attempt; the snapshot is still written before the server exits. -
Learning from feedback: review fixes. A finding posted twice on one pull
request keeps a thumb given on either comment (reactions are read per finding,
not per comment, and a thumb is withdrawn only when no comment carries it); a
question or a quoted keyword is no longer read as a dismissal or an accept, and
a bare-1/+1counts only as the whole comment; a verdict from the reviews
page is matched to the stored finding by file, title and rule; one unreadable
comment no longer stops the reaction poll; erasing a person who signalled under
both e-mail and user id no longer collides on the signals table. -
The answer to
business-logicis cleaned like a chat answer. It quotes a task and a
diff written by other people, so before it is posted as the bot its markers, images, raw
HTML and live @-mentions are neutralised. -
A push no longer deletes a finding somebody asked about through the bot's own account.
On an install where the token is a person's account, the question under a finding is
written by the same account as the finding. A comment of the token's account with none of
Celmis's markers now counts as a person's words and protects its thread from the cleanup,
on all three providers. -
howtoand the full/mcpserver no longer start every repository's graph on each
call. Working out which repositories a caller may reach listed them through a call that
opens each repository's graph to count its symbols (a graph server start and stop, seconds
each), so ahowtocall cost about four seconds per repository in the workspace (sixteen
seconds with four repositories). It now lists the checkouts on disk by name. Measured on
one repository: 3.7 s to 0.1 to 0.4 s. -
howtoslices start at the code, not at a comment. A library named in a comment or a
docstring ("PyJWKClient cannot load keys behind the proxy ...") counted as a use and
anchored the slice there, so the answer showed a file's header instead of the code that
verifies the token. Comment and docstring lines are now ignored when looking for the
pattern. The header line also reads "indexed just now" instead of "indexed now ago". -
/mcp/devmasks the secrets people actually commit.DB_PASSWORD=value,
password: value,password='value',redis://:value@host, JSON"token": "value"
and secret defaults inos.environ.get(...)/process.env.X || '...'are masked
ingrep,read_symbol,refs,findandoutline(a pointer such as
${DB_PASSWORD},settings.db_passwordorchangemeis left readable). Output
is redacted before it is cut to a line or signature length, so a secret straddling
the cut is no longer half-printed.outlineprints names only for a repository the
caller may see as metadata. -
/mcp/devcursors are about 20 characters whatever the number of repositories
(they were several kilobytes over a large fleet) and are pinned to the revisions of
the repositories that produced hits, so a push elsewhere does not reset page 2. -
findkeeps the best candidates on large repositories. Candidates were the 200
shortest names; they are now ordered by how the name matches, then by how often it
is called, so a typo or a common word still reaches the symbol that is used most. -
grepand the sibling-repository text refs say when the indexed revision is no
longer in the clone instead of answering "no matches";repospages exactly as many
repositories as theidx:line can describe;asknever lists a secret file as a
source; root-level.env,.npmrcand similar are recognised as secret paths; a
symbol that lost its last caller loses its usage rank on the next index pass. -
search_symbolsfinds what was meant.kindfilters inside the query before
the limit (twenty variables calleduserno longer hide the one function),
matching is ranked and tolerates a typo (mode: auto, exact, prefix, fuzzy),
repositories are interleaved,end_lineis returned,project_idis optional,
and the answer no longer lists repositories the caller may not read
(blocked_repos,access_notice,hidden_symbol_countare gone from
search_symbols,find_consumersandget_api_surface). -
An incremental index no longer fails for ever on a busy repository. When the
shallow clone has moved past the revision the index was built from, the pass
rebuilds from the checkout instead of asking git for a diff that cannot be taken. -
Tool descriptions over 300 characters on
/mcpare shortened. -
howtofollows the code into its config loader. A connection or auth pattern
that readsconfig.databaseUrlnow also shows the loader that reads
process.env.DATABASE_URLoros.Getenv(...)(TypeScript, Go and others, not only
pydantic settings), lists the names it reads that belong to the topic, and the
secret-store path it names. Helper calls such asrequired("DATABASE_URL")count
as reads inside a loader file. -
Credential files named
<anything>credentials<anything>.jsonare never indexed or
returned, and a JSON file whose content says"type": "service_account"is
refused by the content check whatever its name. -
The
celmis-mcpskill gave the tool count as 18; the server has 48 on/mcp, and the
developer profile adds nine more on/mcp/dev. The skill now says so. -
A refused finding folded into the summary no longer breaks it. A body cut inside a
code fence is closed again, so the rest of the summary (and Bitbucket's tag
rewriting) is not swallowed as code; a malformedRetry-Afteron Bitbucket no
longer aborts the review; the GitHub review body and the classic layout follow
the review language. -
Bitbucket: an inline comment the API would not place is no longer lost.
A refused finding is folded into the summary comment ("Findings without a
place in the diff") with its position and explanation; GitLab does the same
for a refused discussion. -
Bitbucket: comments on unchanged lines are anchored by both sides (
from
andto), a 429 is waited out once (honouringRetry-After, capped at 30 s)
before the comment counts as refused, and the description update sends back
every field it read (draft,close_source_branch) so a rewrite does not
reset them. -
A Bitbucket pull request is no longer "skipped, no diff" when it has one.
Bitbucket answers/pullrequests/{id}/diffwith a 302 and an empty body;
the guarded client never follows a redirect on its own, so the diff read as
empty and the pull request got only a quiet "no diff content" note. The provider
now follows the redirect itself — at most three hops, same host only — asks
fortext/plainand reads the bytes as UTF-8. When the diff is still empty
and the diffstat lists files it retries (by commits, then 2 s and 5 s later)
and then fails the run and says so on the pull request; a PR with no files
stays a quiet skip. When the diffstat cannot be read either, the run fails
with a sentence of its own (the diff could not be verified) instead of
skipping quietly, and a transport error on a Bitbucket read is reported as
"Bitbucket request failed ()" without the exception's text. GitHub and GitLab treat a 3xx as an error instead of an
empty answer, and both now report how many files the PR has
(PullRequest.reported_files), which the "check reviewable changes" gate
uses to tell a lost diff from an empty change-set. -
Cyrillic (and other non-ASCII) file names are anchored. git writes such
paths quoted with octal escapes in diff headers; they are decoded, so hunks,
findings and inline comments name the real file. -
Applying a fix on Bitbucket follows the redirect on the file read and
quotes the path. -
Editing a pull request's title or description no longer starts another
review of a commit that is already reviewed (Bitbucketpullrequest:updated,
GitLabupdate). A failed or skipped last run, a new commit and a PR never
seen are still reviewed. This also keepssummary_target=descriptionfrom
re-triggering itself. -
Integration pass over the new surfaces.
- Comments written through the token owner's account are that person's: the
reviewer ignores only comments that carry its markers or come from a bot
account, so a person on a personal token can use the commands and teach it. - The business-logic agent reads the whole pull request, also in an
incremental review, so a requirement met by an earlier commit is not
reported missing. - A thread the reviewer resolved itself after a push is not read as feedback
from a person, and a reply, thumb or resolved thread from somebody who may
not command the reviewer in that repository teaches nothing. - The guide at the end of the completed comment lists only the commands the
repository has not switched off (chat, memories, task context), and the
wording ofstart-reviewandreview --forcesays what each really does. - "Same commit already reviewed" is decided only by the commit of the last
complete, posted review, so a failed or partial run is retried. GET /api/pull-requests/{id}/commandsanswers 404 to somebody who may not
read the repository.- Memories hide the repository, pull request and author they were taught from
when the reader may not read that repository. - A redundant index on the productivity events table was removed from its
migration (the model never had it), so the migration chain and the models
agree; every new revision downgrades.
- Comments written through the token owner's account are that person's: the
Tests
-
scripts/dev_mcp_journey.pyruns the whole developer journey over real HTTP on an
in-process Celmis: users in six roles, teams and rules over REST, tokens issued by the
superadmin over REST (developer A, developer B, expired, revoked), the scenarios through
a real MCP client with per-call size and latency against a grep-and-read baseline, a
security matrix (84 checks), and a leak scan of every captured output, log and audit row.
--realruns the three developer requests on clones of real local services and keeps the
results outside the repository. Two scenarios were added to the gold file (auth like
service X; credentials of service X used in service Y). -
Leak tests plant runtime-generated fake secrets in a synthetic repo (DSNs, headers,
YAML/JSON, PEM, base64, URL-encoded) and assert none reaches the output ofhowto
or of any existing tool; a table of every tool on both servers fails when a tool is
missing from it, and a negative control proves the check can fail.
Integration review fixes
- The pull-requests list names only repositories the caller may read.
GET /api/pull-requests(rows, the total and the repository facet) filters by
the same per-repository permission as the other review routes; a member no
longer sees titles and authors of a closed repository. - REST and MCP read a repository the same way. When an access rule names only
another team, a team that holds a read grant on the repository is now refused by
REST too, as MCP already did: a rule narrows, a grant is only the fallback. - The repository groups API is scoped. The list shows only the repositories
the caller may read (a group with none is hidden, except from workspace
managers); creating, renaming, adding to, removing from and deleting a group
needs editor or above andreviewpermission on every repository in it. A
group the caller cannot see answers 404. - An access rule names a registered repository.
PUT /api/access/rules
resolves the slug (provider:owner/name, a bareowner/name, or the registry
slug) to the registered one and answers 404 for an unknown repository, so a
rule can no longer be written against a spelling that never matches. - A self-service token carries exactly the write scopes it was granted, not
every write scope the profile allows. - Redaction ReDoS closed. The central redactor and the secret scanner no
longer take quadratic time on a long run of whitespace: runs longer than 64
whitespace characters are collapsed before matching and every whitespace
quantifier is bounded. One crafted file could pin a worker for minutes. - More real secret shapes are redacted: markdown table rows and bold or
code-quoted names, XML name/value pairs in either order, SQL
PASSWORD '...'andIDENTIFIED BY,sshpass -p, andnetrcentries. - The literals floor reaches every model-bound text.
howto, the PR chat,
the cross-repository drift check and the exploration agent mask the values of
secrets the instance itself knows, on top of the central rules. The floor moved
tosrc/security/secret_literals.py. - Two more grep oracles closed. The developer profile's
grepand the
exploration agent'sgrep_in_filematch against the redacted text, so whether
a guessed prefix "hits" no longer says anything about the hidden value. - The issue resolver's first check of a branch no longer waits on another
pass. On PostgreSQL the state row is created in its own transaction with
ON CONFLICT DO NOTHING, so a second pass sees the row locked and reports
busy at once. - The productivity migration is idempotent.
e3b8d0f2a478skips tables and
indexes that already exist and its downgrade tolerates a second run, so a
database that had them created by hand upgrades cleanly. - The compose file forwards every documented setting the API reads (MCP
limits, the previous JWT secrets, trusted proxy, extra secret path globs, the
background-job intervals and budgets, the PR chat and bot-handle settings and
the learning thresholds). The API service has noenv_file, so a variable not
listed there never reached it. - Docs corrected.
@celmis pausenever existed (the Pause button does; the
deadcommandpause reason is gone);@celmis remember:trust follows the token
owner andmemory_trusted_commenters, and workspace roles apply on the
Memories page; the productivity API lives under/api/analytics/productivity;
the MCP skill andserver.jsonteachissue-token --user --workspace --repos --days; a refused token answers 403 with the reason; the howto scope is
read:graphon/mcp/andread:codeon/mcp/dev/; the MCP tool count is 49
(33 read, 16 write) everywhere, and a test now reads it from the loaded tool
map instead of the source literal; the Jira, learning, issue-sweep and
productivity settings are documented; the plugin README saysCELMIS_URLmust
behttps://. - A tracked merge leftover (
models.py.orig) was removed and*.origand*.rej
are ignored;scripts/dev_mcp_journey.pyandscripts/dev_mcp_e2e.pyare
executable.
Upgrade notes
- Every GitHub, GitLab and Bitbucket webhook installed before this release lacks the
comment, thread, approval or push events (see the BREAKING section for the list per
provider) until it is repaired: use "Repair all webhooks" on the webhooks page. The
installer subscribes new hooks only, so a hook nobody repairs keeps reviewing but
ignores@celmiscommands and feedback. - The database moves in one chain with a single head. From v2.3.5 run
alembic upgrade head: twelve revisions apply in order, ending at
b3e9d27f5a40, and each one downgrades. The MCP token and audit tables sit
between the feedback-learning revision and the index-branch column. - Before the first start of this release: issue per-person MCP tokens, and run
analyzer access bootstrap --team <team> --visibility code --all-unruledfor
the teams that need their old access (see the BREAKING section). - The webhook repair above, the per-person MCP tokens and the access bootstrap are the
only manual steps. CELMIS_UNRULED_REPO_ACCESS=openalso restores the old reading of the review
features on a single-tenant install. It is a stopgap.