Skip to content

v2.3.7

Choose a tag to compare

@github-actions github-actions released this 07 Oct 03:28
· 42 commits to main since this release

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 (in single_tenant
    through both the REST API and MCP, in multi_tenant through 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-unruled it 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/unruled and a banner on the Team pages
      say how many repositories are affected.
  • A repository the caller may not read is answered like one that does not
    exist.
    MCP tools no longer return blocked_repos, access_notice,
    hidden_symbol_count or 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/my and /api/access/rules list only what the caller
    may read.
  • MCP tokens are issued by the superadmin, per person, with an explicit
    repository list.
    POST /api/admin/mcp-tokens takes the person, the
    workspace, slugs and/or globs, an expiry (capped by
    CELMIS_MCP_TOKEN_MAX_DAYS, default 90) and allow_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=accept accepts them, read-only, for a
      transition; reissue them instead.
    • analyzer mcp issue-token now requires --user, --repos and
      --workspace (it used to take --subject and --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 at GET /api/admin/mcp-calls (CSV with ?format=csv) or on
    the Calls tab. Retention is CELMIS_MCP_AUDIT_RETENTION_DAYS (default 180).
  • Two migrations, a7c41e9b2d10 (mcp_tokens) and a7c41e9b2d11
    (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-lived internal grant (a row in mcp_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_run and
      list_alerts now 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_sbom for a repo-limited token
      must name one listed repository.
    • ask_code no longer returns blocked_repos and 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 a mcp_call_log row (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-token and analyzer access bootstrap write audit
      actions; args_hash is 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_review treats 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 like acme/shop matches the
      same repository on two providers.
  • 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_thread and
    push; on GitLab note_events; on Bitbucket pullrequest:comment_created,
    pullrequest:comment_updated, pullrequest:approved, pullrequest:unapproved
    and repo: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. Raise CELMIS_MCP_RATE_PER_MINUTE,
    CELMIS_MCP_MAX_CONCURRENT or CELMIS_MCP_CALL_TIMEOUT_SECONDS (0 switches the
    rate limit off); the compose file forwards all three.
  • migrate_consumers no longer takes user_id. It acts as the caller. A
    client or script that passed user_id must 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 @celmis that 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 own pr_chat
    surface on the Usage page; a workspace over its hard-stop budget gets one sentence. New
    knobs REVIEW_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 gained get_thread.

  • @celmis remember teaches the reviewers from a pull request comment.
    @celmis remember: <rule> stores a team memory for the repository,
    --org for the whole workspace, --dir=src/api (or --dir on 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 in help now 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; full reads 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 record scope and
    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 --force and help work from a comment.
    Comment webhooks (GitHub issue_comment and pull_request_review_comment,
    GitLab Note Hook, Bitbucket pullrequest:comment_created and
    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 (@celmis or the
    /celmis alias, from REVIEW_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-review resumes a paused pull request and
    reviews the latest commit; review --force reviews 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_enabled and chat_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.py sends a signed synthetic comment to
    a running instance. Migration a9d4f6b8c034.

  • 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, @celmis plus 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 new learned_filter stage 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 or proven finding 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; on leaves them out; off) and learning_excluded_reviewers
    (people whose feedback teaches nothing). A correction in a reply also offers
    a pending memory. /memories gains 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 gained list_comment_reactions. Migration c1f6b8d0e256.

  • 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 dismiss or 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 follows command_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
    to pull_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_stats over 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;
    in checklist mode one short model call judges the rest, in findings mode
    nothing more is spent and the section says the criteria were not checked one
    by one, and off adds nothing. The mode is the inheritable setting
    requirements_check_mode (default checklist), 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 sets task_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_context channel, judges the whole pull request also after an
    incremental review, and the on-demand command honours task_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_pause pauses a PR after auto_pause_pushes pushes
    (3) inside auto_pause_window_minutes (15), manual reviews 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, force on the trigger route) also skips the
    target-branch rule. One ReviewRequest (trigger, force, scope, resume) now
    carries that through the webhook, the poller, the queue and the orchestrator.
    New run stages gate_title and gate_cadence; the draft gate now runs before
    the repository context is built, so a draft costs no context. Migration
    e7b2d4f6a812 adds the four columns to both policy tables and the pause state
    to review_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/repos carries target_branches and target_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. find ranks 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_symbol prints exactly the
    lines of one symbol from the indexed commit, with the middle elided past a
    limit; grep reads committed text only, never secret files, and names the
    enclosing symbol. The scope is read: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_surface reads routes from source. FastAPI/Flask decorators, Express,
    Laravel, Symfony and Go registrations are found with one git grep at the
    indexed revision (heuristic, and it says so); a repository with no readable
    revision answers supported: false instead of an empty list.

  • The index remembers which branch it indexed (repo_index_state.indexed_branch,
    migration b3e9d27f5a40), 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/ with Authorization: Bearer ${CELMIS_TOKEN},
    and ships the celmis-search skill (search order, howto rules, freshness,
    cursors, guardrails), a read-only celmis-explore agent, 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
    version in plugin.json, so a plugin update follows each commit. Docs:
    packaging/claude-plugin/celmis-code/README.md, including the
    headersHelper form (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.py runs 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; set CELMIS_E2E_STRICT=1 to make a skip a
    failure. ./scripts/e2e.sh mcp_dev is 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-scan makes "not armed" a failure),
    --min-scenarios N fails 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's inputs section. 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 commit abcdef1 (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 as classic.
    All of it speaks the repository's review_language through the message
    catalog (en, uk).

  • Two new review settings, wired through every layer. completed_comment
    (completed by default, or classic) and commands_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), migration d6a1c3e5f701.

  • 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 migration f8c3e5a7b923), 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_commenters list), 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 /memories page
    (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 with memories_enabled,
    knowledge_approval and memory_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/memories endpoint (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/issues takes scope, resolution, outcome and
    include_duplicates, and /summary carries 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) and issues_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-6066 in 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
    with GET /rest/api/3/myself before 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_found switches the
    agent on for pull requests whose task was read; naming it in
    disabled_agents still 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 gained fetch_commit_messages.
    Who can make the bot read a task: the connection is one workspace token, so
    with task_project_keys empty a key written in a pull request is read from
    any project that token can browse, whoever wrote the pull request. Set
    task_project_keys to 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 /productivity page (Enterprise, under the
    analytics licence 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/productivity and are absent without the licence; capabilities
    reports the productivity feature 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; migration e3b8d0f2a478) and the
    writers that fill them, in src/productivity/. Opt-in per repository: nothing is read
    from a provider until enabled is set. Settings layer repo row, then workspace row,
    then built-in (settings.py). sync.py runs 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>, likewise prodpr-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 out size; 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 the content.html of 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 &amp;
    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 summary instead
    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_language carries 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, Kubernetes secretKeyRef, 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 the read:graph scope (carried by every token kind), and exposed as register_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 = value assignments in code, YAML,
    JSON and properties, PEM blocks, provider keys, Kubernetes Secret data 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
    ignores redaction_enabled. output_guard.install_output_guard applies 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.py classifies a path as deny (.env*, private keys,
    credential stores, *.tfstate, kubeconfig, kind: Secret), keys_only (names kept,
    values masked) or ok. 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, and GIT_PATHSPEC_EXCLUDES keeps them out of git-backed scans.
    secret_path_globs_extra adds repo-specific patterns.

  • Review fixes for howto and the redactor.

    • A committed symlink is never followed: .env behind a link, or a file outside the
      clone, was readable through howto and 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, *.keytab and
      *.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, SEED and {"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 AccountKey and SharedAccessKey, Slack, Discord, Teams and
      Telegram webhook secrets, pre-signed URL signatures, curl -u, mysql -p,
      --password x, Cookie and Set-Cookie, and user:pw@host/db without a scheme.
    • The detailed howto answer 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.

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}/runs used 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 below code (metadata,
    none) now also narrows a team grant on the REST side, as it already did in
    MCP; owners and admins are never narrowed. metadata still 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-sessions refuses a repository the caller may not read;
    GET /api/repos/developers lists 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_revoked in the answer).

  • migrate_consumers no longer takes user_id. It acts as the caller,
    needs the owner or admin role of the workspace (or a global admin) and code
    access to each repository it changes. Breaking for clients that passed
    user_id.

  • ask honours the repository list of its token, like every other tool.

  • grep is 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, .npmrc tokens, docker login -p, setter and
    define('X_PASSWORD', ...) calls, Pwd= in code strings, Dockerfile ENV,
    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.txt and similar names; .env ,
    .env. and names with zero-width characters no longer slip past.

  • howto output 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-header will not
    print a token for a CELMIS_URL that 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) and CELMIS_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 with effective_access for 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, PUT and DELETE on
    /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. A list_repos over 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 / +1 counts 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-logic is 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.

  • howto and the full /mcp server 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 a howto call 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.

  • howto slices 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/dev masks the secrets people actually commit. DB_PASSWORD=value,
    password: value, password='value', redis://:value@host, JSON "token": "value"
    and secret defaults in os.environ.get(...) / process.env.X || '...' are masked
    in grep, read_symbol, refs, find and outline (a pointer such as
    ${DB_PASSWORD}, settings.db_password or changeme is 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. outline prints names only for a repository the
    caller may see as metadata.

  • /mcp/dev cursors 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.

  • find keeps 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.

  • grep and the sibling-repository text refs say when the indexed revision is no
    longer in the clone instead of answering "no matches"; repos pages exactly as many
    repositories as the idx: line can describe; ask never lists a secret file as a
    source; root-level .env, .npmrc and similar are recognised as secret paths; a
    symbol that lost its last caller loses its usage rank on the next index pass.

  • search_symbols finds what was meant. kind filters inside the query before
    the limit (twenty variables called user no longer hide the one function),
    matching is ranked and tolerates a typo (mode: auto, exact, prefix, fuzzy),
    repositories are interleaved, end_line is returned, project_id is optional,
    and the answer no longer lists repositories the caller may not read
    (blocked_repos, access_notice, hidden_symbol_count are gone from
    search_symbols, find_consumers and get_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 /mcp are shortened.

  • howto follows the code into its config loader. A connection or auth pattern
    that reads config.databaseUrl now also shows the loader that reads
    process.env.DATABASE_URL or os.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 as required("DATABASE_URL") count
    as reads inside a loader file.

  • Credential files named <anything>credentials<anything>.json are 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-mcp skill 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 malformed Retry-After on 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
    and to), a 429 is waited out once (honouring Retry-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}/diff with 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
    for text/plain and 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 (Bitbucket pullrequest:updated,
    GitLab update). A failed or skipped last run, a new commit and a PR never
    seen are still reviewed. This also keeps summary_target=description from
    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 of start-review and review --force says 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}/commands answers 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.

Tests

  • scripts/dev_mcp_journey.py runs 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.
    --real runs 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 of howto
    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 and review permission 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 bare owner/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 '...' and IDENTIFIED BY, sshpass -p, and netrc entries.
  • 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
    to src/security/secret_literals.py.
  • Two more grep oracles closed. The developer profile's grep and the
    exploration agent's grep_in_file match 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. e3b8d0f2a478 skips 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 no env_file, so a variable not
    listed there never reached it.
  • Docs corrected. @celmis pause never existed (the Pause button does; the
    dead command pause reason is gone); @celmis remember: trust follows the token
    owner and memory_trusted_commenters, and workspace roles apply on the
    Memories page; the productivity API lives under /api/analytics/productivity;
    the MCP skill and server.json teach issue-token --user --workspace --repos --days; a refused token answers 403 with the reason; the howto scope is
    read:graph on /mcp/ and read:code on /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 says CELMIS_URL must
    be https://.
  • A tracked merge leftover (models.py.orig) was removed and *.orig and *.rej
    are ignored; scripts/dev_mcp_journey.py and scripts/dev_mcp_e2e.py are
    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 @celmis commands 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-unruled for
    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=open also restores the old reading of the review
    features on a single-tenant install. It is a stopgap.