> ## Documentation Index
> Fetch the complete documentation index at: https://docs.memory.anonalabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub

> Capture what your merged pull requests decide, and write what a space has learned back into the file your coding agents already read.

Anona's GitHub App connects a repository to a memory space, in both
directions.

**In.** Capture records what a pull request settled — its title, body and
review verdicts on merge, or a whole thread when somebody asks for it — into
the space the repository is bound to. Mention the app in a pull request or an
issue and it answers from that space, under an audience rule that never posts
memory into a thread a wider audience can read.

**Out.** The *knowledge pull request* takes what the space has accumulated and
writes it into one file in the repository — the `AGENTS.md` or `CLAUDE.md` your
coding agents already read — as a pull request your team reviews and merges.
It is off until you choose that file.

## Install

1. Open **Integrations** in the dashboard.
2. On the **GitHub** row, choose **Connect**.
3. Approve the install, and grant it the repositories you want.

Installing captures nothing. Until you bind a repository, the app does nothing
anywhere.

Only an owner or an admin can connect or disconnect an installation. Binding a
repository is open to any member, because you can only bind a space you can
already reach.

## Bind a repository

On the **GitHub** row, open **Bind a repository**:

**Repository** `string` *required*

One of the repositories you granted the installation. Fixed once bound — a
repository's id is what identifies the binding, so changing it would create
a second binding rather than move this one.

**Space** `string` *required*

Which memory space this repository writes to and reads from.

**API key** `string` *required*

The key the app presents. Revoking it stops capture for this repository.
Only live keys are offered.

**What this repository may do** `string`

**Read only** captures nothing. **Read & write** lets capture write into the
space. A ceiling, not a grant: choosing **Read & write** requires that you
can write to that space yourself.

**When to capture** `string`

**On merge** captures a pull request once it merges. **On request** captures
a thread when somebody asks — "@anona-memory remember this". **Both** does
both. Described in full under [Capture](#capture).

**Knowledge pull request** `string`

Which file Anona keeps a section of up to date. **Off** by default, and
leaving it off is a normal configuration. See below.

Binding the same repository again edits it rather than creating a second one.

## The handle

The app answers to **`@anona-memory`** — the handle of the GitHub App itself,
at [github.com/apps/anona-memory](https://github.com/apps/anona-memory).
Mention it in a comment on an issue or a pull request, or in an inline review
comment.

> [!WARNING]
>
> **`@anona` is deprecated. It still works today, and it will be removed.**
>
> `github.com/anona` is a real GitHub account belonging to somebody with no
> connection to Anona, so a comment written that way notifies a stranger who
> never asked to be in your pull request. Nothing on our side caught it:
> Anona matches the text of your comment and never asks GitHub who the
> mention resolved to, so the bot answered correctly the whole time the
> notifications were going somewhere else.
>
> The old handle is kept working only so that the comments already sitting in
> your threads, and anything your team has already written down, do not break
> on the day the app was renamed. Everything below uses `@anona-memory`.

## Capture

Capture is what a bound repository puts *into* its space. There are two
triggers, and **When to capture** on the binding decides which are live:

| Setting | `capture_mode` | What triggers a capture |
| - | - | - |
| On merge | `merge` | A pull request is merged |
| On request | `explicit` | Somebody comments `@anona-memory remember this` |
| Both | `both` | Either |

`merge` is the default, and a **Read only** binding captures nothing under any
of them.

There is deliberately no fourth setting that captures every comment. Turning
prose into memories runs on one extraction queue shared by every Anona
customer, so a repository streaming its whole comment traffic through it would
hold up everyone else's ingestion and not only its own. `both` is as wide as
this is allowed to go, and the hourly cap further down is the same constraint
seen from the other end.

### On merge

When a pull request **merges**, Anona captures its title, its body, and the
conclusion of each review on it — "alice approved", "bob requested changes:
the migration has to run first".

Closing a pull request without merging captures nothing. Most closed pull
requests were abandoned, and a decision nobody took is not worth keeping.

What is kept is who wrote it, who merged it, the title, the body, the
conversation, and each reviewer's verdict. The per-line inline review replies
are never read — those are the "nit"/"done"/"rebased" chatter that a diff
accumulates and nobody recalls.

A review that **approved** or **requested changes** is kept even when the
reviewer wrote nothing, because there the verdict is itself the decision. A
review that only commented, with no text, is dropped.

Every voice in a captured thread is named, including the person who opened it,
so "who wrote this?", "who merged it?" and "who commented?" all have answers.
A very long conversation is trimmed to its most recent comments and says so in
the memory itself — whole comments only, since half a comment is a sentence
that stops mid-clause and would be recalled later as a fact.

**Only what the project vouches for.** GitHub stamps every pull request and
review with the author's relationship to the repository, and capture keeps only
those written by the owner, a member of the organization, or a collaborator.
Anything else — an outside contributor, a first-time drive-by, a relationship
GitHub does not report at all — is left out.

The two halves are filtered separately, which is the useful outcome: an outside
contributor's pull request that your team reviewed keeps your reviewers'
verdicts and drops the contributor's prose. If nothing survives the filter,
nothing is written and nothing is billed.

This is a trust boundary rather than tidiness. On a public repository anybody
can open a pull request, and captured text becomes memory the knowledge pull
request may later render into a file your coding agents read as standing
instruction. Capture is the last point at which it is still knowable which pull
request a sentence came from, so it is the only place the filter can go.

Pull requests **opened by a bot are never captured** — including Anona's own
knowledge pull request. Merging that one is the whole design, and ingesting it
would feed a synthesis back in as fresh evidence for the next synthesis to
build on.

### On request

Comment on an issue or a pull request:

```
@anona-memory remember this
```

and Anona captures **the whole thread**: its title, its body, and every human
comment on it — not only the comment that asked. A decision lives in the back
and forth, and keeping the conclusion alone throws away the reason for it.

Also understood: `save this`, `record this conversation`, `remember the above`,
`store this discussion`, `note this`, and similar. The instruction has to be
**the whole comment** — the mention, the instruction, and nothing else. A word
of address may come first, so `please @anona-memory save this` is the same
instruction; "did" is not one of those words, which is what keeps
`@anona-memory did we ever record that?` a question rather than a silent
save.

It also means a comment that merely *quotes* a directive is not one — and it
is not a question either. What counts as addressing Anona is what the
commenter typed, not what GitHub's **Quote reply** button pasted in above it,
so a comment that is entirely quote is met with silence: nothing was addressed
to us, so there is nothing to decline and nowhere a decline would be welcome.
A quote with your own `@anona-memory …` underneath is a question, answered as
one. A quote with a fresh directive underneath is a save.

Two things have to be true:

* **Your GitHub account is connected to Anona** in the dashboard. Until it is,
  there is no person to attribute the memory to and no membership to check, so
  the directive is declined.
* **You can write to the bound space yourself.** Checked at the moment of the
  capture rather than at bind time, so losing write access stops it
  immediately. The binding's **Read & write** setting is a ceiling on top of
  that, not a substitute for it.

Anona's own comments are never captured, and neither is any other bot's — CI
status, Dependabot, stale-bot. An answer is a synthesis of what the space
already holds, and writing it back in as fresh evidence is a photocopy of a
photocopy.

Every *human* comment on the thread is kept, whoever wrote it. The
vouched-for filter that merge capture applies does not run here: somebody who
can write to the space asked for this thread by name, and that judgement is
what the filter stands in for when nobody made one.

The directive works in an issue comment and in a pull request's conversation.
It does **not** work in an inline review comment on a line of code.

#### Reactions

Anona never comments back when you ask it to remember something — a comment on
every save is noise on a page people are trying to review code on. It reacts
to the comment you wrote, the way a person would:

| Reaction | What it means |
| - | - |
| 👀 | Taken. The thread is queued for extraction, which runs in the background |
| 👀 👍 | Stored. The memory exists and is recallable |
| 😕 | Declined. Nothing was saved |
| 👀 😕 | Taken, then extraction failed. Nothing was saved |

The two-step is deliberate: extraction is not instant, and 👀 is the difference
between "Anona has it" and "Anona never saw it".

**They accumulate. They never replace each other.** GitHub can add a reaction
and it can remove one, and removing is a second call that can fail and leave a
stale acknowledgement standing where a person will read it as the truth — so
nothing is ever removed. They are a trail, which is what makes 👀 on its own,
👀 👍 and 👀 😕 three distinct states rather than one that keeps being
overwritten. There is no reaction meaning "still going"; the absence of the
second one is that.

The second reaction arrives once the write finishes, which can take a little
longer while a deploy is in progress. If Anona cannot tell what became of the
write, it adds nothing, and the 👀 alone stays *true* — it says queued, and a
write that has not finished is still queued. That is the whole reason the
accepted state is two reactions and not one: the honest outcome of losing
track is an acknowledgement that has gone stale, where a single terminal
reaction would have had to either lie or never arrive.

**The 😕 never says why**, because a pull request thread can be the whole
internet and "you do not have access to that space" would tell it that the
space exists and that you cannot read it. The reasons are a binding set to
capture on merge only, a read-only binding, a GitHub account that is not
connected, the hourly cap, an Anona account out of credits, a revoked API key,
no write access to the bound space, GitHub refusing to hand over the thread,
a thread with nothing in it to save, and a write the memory engine would not
take. Which one it was is in the Anona dashboard, behind your session.

Reactions have an hourly allowance of their own — **a hundred and twenty per
installation**, counted across every repository bound to it and separate from
both the capture cap below and the reply budget further down. Separate so that
a repository being drowned in directives cannot silence its own questions, and
generous because a reaction, unlike a comment, costs a thread's readers no
attention at all. Past it Anona reacts to nothing, which looks exactly like the
integration being broken: it is the one case where no reaction does not mean
nothing happened.

A **merged pull request** is captured with no reaction at all. Nobody asked for
it, so there is nobody waiting to be told.

### What capture posts back

The only thing Anona puts back on a thread it captured is the reaction above,
and only when somebody asked. It never opens a comment to report an outcome —
not a save, not a skip, not a cap.

A merged pull request is captured silently, so the space itself is where to
confirm one landed. Extraction is asynchronous, and a memory does not appear
the instant a pull request merges.

### Duplicate captures

A pull request captures once; a comment captures once. A redelivery, or two
events racing each other, collapse to that one capture. Editing a comment into
a directive after the fact does not capture it either — only a freshly posted
comment counts.

### Importing recent history

Binding a repository imports none of its past. Capture is driven by webhooks,
so it begins at the *next* merged pull request — everything merged before you
bound the repository stays uncaptured, permanently, unless you ask for it.

**Import recent history** on a bound repository asks for it. You choose how
much: a **count of the most recent merged pull requests** — 50 by default, up
to 500 — or **all of them**. Either way they are captured exactly as a merge
would be: same filter on who wrote what, same bot exclusion, same charge.

A count rather than a date range, deliberately. Ninety days of a quiet
repository is five pull requests and of a busy one is eight hundred, so a date
bounds nothing that matters; a count bounds the cost, which is the thing worth
bounding. Newest first because recency is the best available guess at
relevance.

The dialog says how many merged pull requests the repository actually holds, so
the count you pick is against a real number rather than a guess.

**Nothing is spent until you confirm a number.** Ticking the control quotes
first: how many pull requests would be imported, what they would cost at most,
and what your balance would be afterwards. The cost is an exact ceiling, not an
average — a capture is charged a flat `add`, so the arithmetic is the count
times that. Some of the matched pull requests will cost nothing at all (a bot
author, nothing left after the author filter, one already captured), so the
real spend can only come out lower.

An import that your balance cannot fund is **refused before it starts**, not
run half way. A job that dies at 60% having spent your allowance is worse than
one that never began, because you have paid for an import you now have to
repeat.

It runs in the background at fifteen pull requests an hour, under the cap
below, starting within the hour of your confirming it. Fifty therefore takes
about four hours, and a whole history of several hundred takes days. Progress
shows on the repository's row — how many have been walked, how many were
captured, and when it last moved — and it survives restarts, so you can close
the page.

**You are emailed when it ends**, whichever way it ends: once with what it
captured, or once with why it stopped and whether starting again would help. A
multi-day import is not something to sit and watch.

Re-importing is safe and is not re-charged: each pull request is claimed before
it is captured, so a second import of an overlapping window skips what the
first already did. Once a window is fully imported the quote comes back as
nothing to import.

Four kinds of repository cannot be imported, and the control says so rather
than quoting a cost it could not honour:

* one bound **Read only** — there is nothing an import could write;
* one set to capture **On request** only — there is no merge history for it to
  take;
* one whose **API key** has been revoked or has expired;
* one that is **already importing** — wait for it to finish, then import again.

What it does not do: issues, and pull requests closed without merging. Both
are what live capture already leaves out.

### Importing a whole history

**Import all pull requests** takes the repository's entire merged history
rather than a window of it. It is a separate tickbox because the cost is not
small and you should see it before agreeing to it: the dialog quotes the exact
ceiling, which for a repository of several hundred pull requests can be
thousands of credits.

It is quoted on **what is left**, not on what the repository holds. Pull
requests an earlier import already took are excluded from both the count and
the price, so importing 50 and then importing the rest costs what the rest
costs — not the whole history over again.

The walk goes **oldest first** and resumes where it stopped, so it survives
restarts and spreads across as many days as it needs. It is bounded at the
start: the newest pull request at the moment you confirm is where it ends, so a
pull request merged during a multi-day import is not chased — the next import
takes it.

**How long it takes is arithmetic, and it is worth doing before you confirm.**
An import runs to fifteen pull requests an hour (see
[the hourly capture cap](#the-hourly-capture-cap) for why fifteen and not
twenty), and the scheduler looks for work once an hour. So:

| Pull requests | Roughly |
| - | - |
| 50 — the default window | 4 hours |
| 200 | 14 hours |
| 500 — the most you may request | a day and a half |

Those are floors, not estimates. The allowance is shared with live merge
capture, so an import on a repository that is actively merging gets less than
fifteen in the hours when merges are landing — which is the intended
behaviour, not a delay to work around.

Nothing is lost to the wait. The job keeps its place, reports progress while it
runs, and emails you when it finishes, so a long import needs no supervision —
you do not have to leave the page open, and closing it does not stop anything.

Nothing is charged twice. Each pull request is claimed before it is captured,
so a pull request a live merge captured while the import ran is skipped rather
than paid for again.

One caveat worth knowing before you import a *count* rather than everything: a
pull request that merges *while* the import is running enters the window and
can push the oldest one out of it. The job says so when it happens, and names
how many. Those displaced pull requests now sit below the window the page asked
for, so a repeat of the same count walks past them — a wider count reaches
them, and importing everything reaches all of them.

### The hourly capture cap

Each **installation** can capture twenty times in a rolling hour, counted
across every repository bound to it and across all three of its consumers
together: merge capture, `@anona-memory remember this`, and
[importing history](#importing-recent-history).

**An import never takes the last of it.** The import runs to fifteen an hour,
not twenty, so five of the allowance are always left for a merge landing while
it runs. The asymmetry is the reason: a merge refused for want of allowance is
gone — the delivery is not retried, so that pull request is never captured —
while an import that is held back simply resumes on the next pass an hour
later. History that has waited months can wait another hour; a merge happening
now cannot.

Fifteen an hour is what makes a large import take as long as it does —
[the table above](#importing-a-whole-history) turns it into hours.

It counts captures that *happened*. A directive that was refused — a read-only
binding, an unlinked commenter — costs nothing against it, so the allowance is
never eaten by people who were never going to be able to save anything anyway.

The cap is real rather than arbitrary — the shared extraction queue again — and
it will rise when that queue stops being shared. Twenty is twice Slack's ten
because merging is automatic: a release window produces more merges in an hour
than anybody tidying up threads by hand ever produces saves.

This is one of two hourly limits, and they ration different things. This one
rations *writing into memory*. The
[reply budget](#the-hourly-reply-budget) rations *comments Anona posts back*,
which is a different resource with a different abuser, and neither one draws
on the other.

### Attribution

If the person who merged, or who asked, has connected their GitHub account, the
memory carries their Anona identity. A merge by somebody who has not connected
one is recorded **unattributed** rather than dropped — losing it because a
contributor never visited a settings page throws away something real.

**Whoever installs the app is connected automatically.** The install already
proves both identities at once: your Anona session starts it, and GitHub's own
authorization proves which GitHub account approved it. Nobody should have to
perform a separate linking step for a connector they just installed, so that
link is recorded there and then.

This matters most for **on request** capture, which — unlike merge capture —
needs a connected account: it has to know whose access to check before it saves
anything into a space. Until a contributor connects theirs, `@anona-memory
remember this` from them is declined.

Merge capture also keeps running on the grant of whoever **bound** the
repository, re-checked on every merge. If that person loses write access to the
space, the repository stops capturing on merge.

**Imported history is always unattributed**, even where live capture would have
attributed it. The list GitHub returns for a history import does not say who
merged each pull request, so there is nobody to attribute it to, and fetching
every pull request individually to find out would cost a request each. This
matters in one case: in a space **shared across organizations**, a read scoped
to one member matches only memories carrying that member's tag, so imported
history is invisible to it while live captures from the same repository are
not. In a space that is not shared, no *member* tag is written either way and
there is no difference to see.

### Where a memory came from

Every memory this connector writes is stamped with tags saying where it came
from, whether it arrived from a merge, from a request in a thread, or from a
history import:

| Tag | Meaning |
| - | - |
| `anona:source:github` | Written by the GitHub connector rather than through the API |
| `anona:repo:<id>` | GitHub's **numeric** repository id |
| `anona:pr:<number>` | The pull request or issue number |
| `anona:author:<id>` | Who **wrote** it, as GitHub's **numeric** user id |
| `anona:merged_by:<id>` | Who **merged** it, same form — usually a different person |

Read where a memory came from on any retrieve result or memory list row, in
its `provenance` object:

```json theme={null}
{
  "memory_id": "...",
  "content": "CD swaps only the image; Terraform ships the env.",
  "provenance": {
    "platform": "github",
    "repo": "1282949371",
    "pull_request": 658,
    "author": "7001",
    "merged_by": "9001"
  }
}
```

`provenance` is `null` for anything written through the API, which is most
memories. Note it is **not** the `source` field on a retrieve result — that one
is the original passage an `include_source` request returns.

Select on it with the `source`, `repo`, `pull_request`, `author` and
`merged_by` fields, on retrieve and on the memory list:

```bash theme={null}
curl "$ANONA/v1/spaces/my-space/memories?repo=1282949371&author=7001" \
  -H "Authorization: Bearer $ANONA_API_KEY"
```

```json theme={null}
{ "query": "how do we handle deploys?", "source": "github" }
```

Several are AND-ed, and a filtered read never matches memories that carry no
provenance — "what came from GitHub" cannot answer with a memory that did not.
Combining `author` with `repo` is what answers "what did this person decide
about this part of the system", which neither field answers alone.

`author` and `merged_by` are separate fields because they are usually
different people, and the two questions have different answers: one is who
wrote the change, the other is who decided to take it.

Both are GitHub's numeric user id rather than the login, for the same reason
the repository is: a login can be renamed and the id cannot. Neither is the
same as `member_id` — that one is who they are to Anona and is set only once
they have connected an account, while these are always known.

Imported history carries `author` but no `merged_by`: GitHub's pull request
list endpoint says who wrote each one and not who merged it.

> [!NOTE]
>
> `author` and `merged_by` were a single field until October 2026, and it was
> stamped from whoever clicked merge. Memories captured before then therefore
> carry the **merger's** id under `author` and no `merged_by` at all. Those rows
> are not corrected in place — re-deriving the author would mean re-reading every
> pull request from GitHub — so a filter on `author` over a repository's older
> history is matching mergers. Captures from October 2026 onward carry both
> fields, each meaning what it says.

The names are in the memory's text as well as in these tags, which is what
lets you simply ask. Tags filter; text is what the answer is built from, so a
numeric id alone could say what a repository decided and never who decided it.
A captured pull request reads like this:

```
Author: bob
Merged by: alice

Add the thing
Because we needed the thing.

Conversation:
bob: I went with the simpler fix.
carol: Why not the other approach?

Review conclusions:
- carol approved: LGTM, ship it
```

> [!NOTE]
>
> You filter with these fields rather than by naming the tags, and that is
> deliberate. The tags live in the reserved `anona:` namespace, which a caller
> may never name — it is the same rule that stops somebody writing
> `{"not": {"tags": ["anona:user:alice"]}}` and reading around another user's
> scope. The fields let you select on provenance without opening that door.

The repository is identified by its numeric id and not by `owner/repo`,
because a repository can be renamed and its id cannot — a name-keyed tag would
split one repository's history in two the day somebody renames it.

These tags are **stamped by Anona, never accepted from a caller.** Tags
beginning with `anona:` are reserved: sending one yourself is rejected with
`422 reserved_tag`. That is what makes the answer to "what did this repository
teach us?" trustworthy — nobody can attribute their own writes to a repository
they do not have access to.

> [!NOTE]
>
> One thing to know if you already filter by your own tags. A query that sends
> `tags` with `tags_match: "any"` matches untagged memories as well as matching
> ones. Captures used to be untagged when the author had not connected a GitHub
> account, so they came back from every such query; now they carry the source
> tags above and come back only when a tag matches. Reads scoped by
> `user_id`, `agent_id`, `session_id` or `member_id` are unaffected — those
> already exclude untagged memories — and so is any query that sends no tags
> at all.

## Ask

Mention the app in an issue or a pull request in a bound repository:

```
@anona-memory what did we decide about the retry policy?
```

The answer is the same synthesis `POST /v1/reason` returns, against the space
the repository is bound to. Inline review comments work too, which is where
reviewers actually argue.

The app **answers only when mentioned**. It never volunteers on an opened pull
request, and it never answers another bot.

There is no "Thinking…" placeholder the way there is in Slack: the thread stays
quiet until the answer or the link arrives. An answer is a multi-step reasoning
loop and takes tens of seconds.

One question at a time per repository. Ask a second while the first is still
running and it says so rather than queueing it.

A **Read only** binding still answers questions. The role governs writing, not
reading.

### The audience rule

A pull request thread is read by whoever can read the repository. So an answer
goes *into* the thread only when Anona can establish that the whole readership
already holds the space:

| Repository | Every collaborator holds the grant | Reply |
| - | - | - |
| private | yes | the answer, in the thread |
| private | no | a link, carrying no content |
| public | not knowable | a link, carrying no content |

**A public repository never gets an in-thread answer** — on any roster state,
and no setting changes that. A public repository has no membership to check, so
nothing can establish that everyone who can read the thread already holds the
space. That is the design rather than a limitation to work around.

**An unreadable or not-yet-synced roster is also a link.** A repository whose
collaborator list has never been read, or whose read GitHub refused, knows
nothing about who is standing in the room — and "I know of nobody outstanding"
must not be confused with "nobody is outstanding". The collaborator list is
re-read when it is more than five minutes old.

A repository GitHub calls **internal** — visible to everyone in your enterprise
— gets the link too. Its readers are the enterprise rather than its
collaborators, so the one list that can be checked does not describe who can
see the thread.

The link reply is a comment saying the answer is in Anona, and it carries
nothing else: no excerpt, no first line, no "I found three memories". Where it
leads is [below](#the-answer-page).

There is one wording for every link-only reason. Distinguishing them — public
repository, somebody outstanding, a roster that would not read — would publish
the roster state into the thread.

GitHub has no ephemeral comment and no direct message. "Answer privately" can
therefore only mean a public comment carrying a link the reader has to
authenticate to open. That is worse than Slack's ephemeral message, and it is
the best the platform offers.

> [!NOTE]
>
> **"Nothing on that in this space yet" follows the audience rule too.** It is
> the result of a search and it reports what the space holds, so on a public
> repository you get a link to it rather than the sentence. Whether a space has
> anything on a subject is the same kind of disclosure as the answer itself.

### The answer page

The link goes to a page in the Anona dashboard. The page is the access check,
not a viewer with one bolted on.

**It resolves your own access to the bound space before it shows anything**,
the same way every other space-scoped surface in the dashboard resolves it. So
forwarding the link — to a colleague, into another thread, out of the company
— hands nobody anything. Somebody without the grant is told they do not have
access to that answer and to ask whoever shared the link, or a space owner, to
invite them. Somebody who is not signed in — the ordinary case, since the link
is followed out of GitHub — is sent to sign in by whichever method their
account uses, and returned to the same page afterwards. **Holding the URL is
not access**, and that is the only reason the reply is allowed to be a public
comment at all.

**It never names the repository, the binding or the space.** It shows the
question, the answer, and when it was answered. Nothing else, and that is
deliberate rather than minimal: naming the space would undo the discretion the
thread's own comment is keeping, and turn a forwarded link into a way to learn
which memory space a repository is wired to.

The answer is stored as it was given. Memory moves, so a page opened a month
later shows what the thread was discussing rather than what the same question
would return today. Ask again in the thread for that.

Unbinding the repository, or disconnecting the installation, retires its
links. The page can no longer establish which space the answer came out of, so
it says the answer is not there rather than showing it to whoever is holding
the URL.

### Refusals

Two different people get the same comment:

* somebody who has not connected their GitHub account to Anona;
* somebody who has, but holds no access to the space this repository reads.

The same reply, word for word, pointing at the dashboard. The refusal is
deliberately identical: naming the second would tell everyone reading the
thread that a particular space exists and that this person cannot read it. The
dashboard, behind your session, says which it actually was.

The generic refusal is therefore not a vague error. It is the only thing the
thread is allowed to say.

### When it cannot answer

These are facts about the integration rather than about the space, so they are
posted in the thread whatever the repository's visibility:

| What you see | What happened |
| - | - |
| out of credits | The paying organization's balance is spent. |
| the API key was revoked | Pick a new one on the binding. |
| that space no longer exists | The bound space was deleted. Re-bind the repository. |
| still working on the last question | Another question is already running in this repository. |
| could not answer that one | Something failed on the way to an answer. Ask again. |
| it can't answer that here | You are not connected, or you do not hold the space — see above. |

### The hourly reply budget

Each **installation** may post sixty replies in a rolling hour, counted across
every repository bound to it: answers, link replies, refusals and every message
in the table above, all out of the one allowance. Past it the app goes quiet.

There is deliberately no "you have hit the cap" comment. **The comment is the
thing being rationed**, so announcing the cap to whoever is driving it would be
exactly the noise the cap exists to stop. Capture can afford to say so with a
😕, because a directive is a deliberate act by somebody who already holds the
space; a question is not — on a public repository anybody with a GitHub account
can post one.

This is the other half of the [capture cap](#the-hourly-capture-cap), and
the two are sized against different resources. Capture's twenty ration the
shared extraction queue. These sixty ration GitHub's rate limit and the
attention of everyone watching the thread.

Sixty is high because the budget binds almost entirely on the paths that cost
nothing. An answered question is a multi-step reasoning loop, already
serialised one at a time per repository, so an hour cannot physically hold many
of them. A refusal to somebody who has not connected their account is free and
can be typed as fast as anybody likes. That is the case being bounded, which is
also why the budget is claimed *before* Anona works out who asked — a question
that was never going to be answered still spends one.

Per installation rather than per repository, for the same reason: somebody with
several of your public repositories to comment on would otherwise get a fresh
sixty in each.

It is counted per gateway process, so across a deployment running several the
real ceiling is a small multiple of sixty. It is a ceiling where there was
none, not a precise number to plan against.

### Restarts mid-question

An answer is produced by a background task, and a background task dies with the
machine running it. When that happens the question is left with no reply at all
— no error, no timeout, nothing at all to tell it apart from having been
ignored, which is the worst shape a failure on this surface can take.

So every time the gateway starts it sweeps for questions that were accepted and
never answered, and posts one comment into the thread that asked:

> Could not answer that one. Try again in a moment.

**It means the machine restarted rather than that the question failed on its
merits.** Nothing here legitimately runs for five minutes — the reasoning loop
is bounded well inside that — so anything still open when the sweep looks
cannot still be running. A question Anona could not answer *on its merits* gets
one of the replies in the table above, straight away. A question it would not
answer *for you* gets the generic refusal, also straight away. This sentence,
arriving minutes late and by itself, is the third thing: the task went with its
container. Ask again.

It is posted in the thread whatever the repository's visibility, for the same
reason the table above is: it carries nothing out of the space.

Only questions are swept. Everything else Anona receives from a repository — a
merge, a push, a label, a review request — leaves nobody waiting, and
apologising in a thread where nothing was asked would be worse than saying
nothing. A merged pull request that failed to capture is therefore silent here
too; look at the space.

If the repository cannot be reached at the moment the sweep runs — the
installation removed, the repository archived, GitHub down — the question is
left open and tried again at the next start, rather than written off as handled
and forgotten.

## Costs

| What | Charged to |
| - | - |
| A capture, on merge or on request | The organization that owns the installation |
| A capture from an [imported pull request](#importing-recent-history) | The organization that owns the installation |
| An answer to a question | The organization of the person who asked |

The split is not an oversight. A merge has no asker — the installation
configured capture, and whoever clicked merge did not request a charge and may
not know Anona exists — so the installation pays, and `@anona-memory remember
this` follows it for consistency. A question has an asker, and every other
Anona surface charges them. For an ordinary team both are the same
organization.

## The knowledge pull request

Once a week — or daily, or monthly, whichever you pick — Anona renders what
your memory space has learned about this repository into a section of one file,
and opens a pull request. You review the diff and merge it, or you close it.
Nothing reaches your repository until somebody with write access merges.

The point is the file your coding agents already read. A synthesis that lives
only in a dashboard is one nobody's agent sees; the same text in `AGENTS.md` is
context on every task.

### Choosing the file

You choose the file, and Anona never picks one for you. When you choose a
repository in the bind form, Anona scans its default branch for the conventions
a team might already keep:

```
AGENTS.md
CLAUDE.md
.cursorrules
.github/copilot-instructions.md
CONTRIBUTING.md
```

What it finds is **offered**, not applied. You pick one, or you type a path of
your own, or you leave it off — and off is what a binding does when you say
nothing. Proposing `AGENTS.md` to a team who maintain a 1,600-line `CLAUDE.md`
by hand is how a bot pull request gets closed unread, so Anona does not
propose; it asks.

Each file on offer says what the first pull request will do to it:

| What the picker says | What the first pull request does |
| - | - |
| No Anona section yet | Adds one at the end of the file |
| Anona already keeps a section here | Updates that section, in place |
| This file's Anona markers cannot be read | Nothing, at any cadence, until somebody fixes them |

Two more entries carry no promise at all, and both are deliberately still on
the list. **Anona cannot write here** means the file is there and can never be
edited — a directory by that name, a file past the size GitHub will serve as
text, bytes that are not UTF-8. **Could not check this file** means the read
itself failed, so nothing at all is known about that one. Neither is quietly
dropped: a file nobody managed to read is not a repository without one, and
telling you what a diff will look like is not a claim a failed read is
entitled to make.

If the scan cannot read the *repository* at all, it says so and offers a
retry. That is deliberately not the same screen as a repository which keeps
none of these files — "we could not look" and "there is nothing there" are
different facts, and only one of them means a path should be typed.

### The delimited section

Everything Anona writes lives between two markers:

```markdown theme={null}
Your own notes, untouched.

<!-- anona:begin -->
## What this repository's history shows
…regenerated in full on every refresh…
<!-- anona:end -->

Also yours, also untouched.
```

The section between the markers is replaced wholesale on every refresh, and
the file is cut at the two markers so the parts you own are put back exactly
as they were, down to the trailing newline — not reformatted, not re-wrapped,
not re-serialized.

**Nothing outside the markers is modified unless you turn on corrections**,
which is off by default and described in [Correcting your own
sentences](#correcting-your-own-sentences) below. With it off, prose kept
outside the markers will still be there in a year. With it on, the only thing
that ever changes outside them is a sentence your memory space contradicts,
and every such edit is wrapped in a tag you can delete to make it permanent.

Three consequences worth knowing:

* A file with no markers yet gets the section **appended** at the end. Your
  existing content is the start of the new file, byte for byte.
* A file that does not exist is **created**, containing only the section.
* A file whose markers cannot be read unambiguously — one half missing, two
  copies, an end before a begin — is **left alone**, and no pull request is
  opened. Anona will not guess which of your paragraphs was meant to be its
  own. This is the one failure here that waiting does not fix, so it is also
  the one recorded on the binding and shown on its row in **Integrations**,
  rather than left to be inferred from a repository that went quiet.

Editing inside the markers is not forbidden, but the next refresh replaces it.
That shows up as a deletion in the pull request's own diff, in front of you,
before you merge.

### What the section contains

Every entry in the section is a quotation, under a heading naming the memory
model it came from — with the question that model answers on the line below,
when it has one — and all of it beneath a preamble that names the space and
says the whole section is evidence. It is evidence, not instruction: a memory
model may well contain a sentence like "always do X", and presented as a
quoted, attributed claim it is something a reviewer can evaluate, where the
same sentence in the document's own voice is something an agent obeys.

At most twenty models are rendered, and a very long one is cut off with a line
saying so and pointing at the dashboard. A pull request nobody can read is a
pull request that gets merged unread, which would defeat the only real
protection this feature has.

The section reads like a document a colleague wrote about your repository.
That is what it is.

### Correcting your own sentences

The section above is Anona's to write. **Corrections** are the opposite: Anona
proposes edits to sentences *you* wrote, where your memory space contradicts
them. A file that says sessions live in Postgres, in a repository whose memory
records the move to Redis, is a file that will mislead the next agent to read
it — and the memory that knows better is already sitting there.

This is **off by default**, and turning it on needs a knowledge file chosen
first: the setting is per repository, beside the cadence. Enabling it without a
file is refused with `github_knowledge_corrections_need_path`.

When it is on, a pass proposes at most **three** sentence rewrites, and every
one of them:

* **carries the memories that contradict it**, quoted with their source, in the
  pull request body. No evidence, no proposal — a correction you cannot check
  is one you should not merge;
* **is wrapped in an `<anona>` tag** in the file, so it is visible in the diff
  and findable afterwards;
* **leaves every other byte alone.** The file is rebuilt from the original text
  plus the corrected sentences, so an untouched line cannot be reformatted or
  re-wrapped even accidentally;
* **never touches the delimited section.** A correction whose span reaches into
  the markers is refused, so the two mechanisms cannot fight over the same
  bytes.

Three is a deliberate ceiling rather than a measured one. The diff a reviewer
reads *is* the merge gate, and a twelve-hunk diff is a rubber stamp.

#### The tag is the opt-out

```markdown theme={null}
Sessions live in <anona>Redis</anona>.
```

Leave the tag and Anona may correct that sentence again later, if the memory
moves on again. **Delete the tag and it never will** — a sentence you have
untagged is one you have taken back, and no future pass proposes it. That is
the whole mechanism: nothing to configure, nothing to remember, and it lives in
the file rather than in a dashboard you would have to go and find.

Closing a corrections pull request is the other way to say no. Whatever it
proposed is never proposed again, even if the memory behind it does not change.
Merging accepts it and leaves the tag in place.

#### What it costs, and when it runs

Corrections ride the same clock as the section — one pull request at a time,
covering both — so a weekly repository gets at most one a week either way. A
pass walks six sections of the file at a time and picks up where it left off,
so a long file is covered over several weeks rather than all at once.

### Cadence

Three cadences, set per repository beside the file:

| Cadence | What it means |
| - | - |
| Daily | At most one pull request a day. |
| Weekly | At most one pull request a week. The default. |
| Monthly | At most one pull request a month. |

**It is a ceiling, not a schedule.** Daily does not mean a pull request a day;
it means Anona may open one as often as once a day, and it opens none at all
unless this repository's memory actually changed.

**There is no per-merge option, and that is deliberate.** The synthesis is
refreshed in the background, so a pull request opened the moment a merge landed
would show you the picture as it was *before* that merge — fresh-looking and
already out of date. More importantly, the merge gate is the only thing between
generated text and a file your coding agent follows, and a bot pull request
that turns up on every merge is one that gets merged unread.

Nothing is opened when:

* the rendered section is **unchanged** since last time (there is deliberately
  no "last refreshed" timestamp in it, so an unchanged week produces an
  identical section and no pull request);
* a previous knowledge pull request is **still open** — one at a time;
* the space has nothing worth rendering yet;
* the binding's API key has been revoked, or the organization is out of
  credits.

Refreshing a memory model that has fallen behind is charged as one `insights`
operation, to the organization that owns the installation. A quiet repository
costs nothing at any cadence — there is nothing stale to refresh.

### Shared spaces

If the space is shared to you by another organization, the knowledge pull
request is refused — the bind form explains it before you pick a file, and the
API answers `403 github_knowledge_shared_space`.

Everyone who can read the repository would see what was written into it, which
is a wider audience than the shared space's own members, and it is not the
binding organization's decision to make. Capture is unaffected; only this
feature is off.

The check runs again on every pass, not just at bind time — a space can be shared
*after* the binding was saved, and a check made only at bind time would never
see it.

### Turning it off

Set the field back to **Off** and save. The dashboard asks first, because the
binding save is a full replace and "I did not touch that field" and "I turned
it off" would otherwise be the same request.

Turning it off stops future pull requests. It removes nothing: whatever has
already been merged into your repository is yours, and stays exactly where it
is. Delete the section by hand if you want it gone.

## The endpoints behind the page

Everything the Integrations page does is also callable directly. **The reads
take either a dashboard session token or an API key; the writes take a session
only** — an API key is refused with `401 session_required`. Which is which is
marked in the table.

The split is not about reads being safer. Every read here needs nothing but
your organization, while every write records *who* performed it, and an API key
names no person. More to the point, an API key carries no role: binding a
repository chooses which space a repository's history flows into, and
disconnecting an installation tears the integration down — both are owner-grade
and stay behind a session and a role check. Scripting them would need scoped
API keys, which do not exist yet.

So an API key is enough to **watch** the integration — assert in CI that it is
still connected, quote an import before paying for it, poll one to completion —
and never enough to change it.

| Method and path | What it does |
| - | - |
| `GET /v1/github/status` | The connected installation and every repository binding. **Session or API key.** |
| `GET /v1/github/install` | The GitHub App install URL to send the browser to. Returns JSON rather than redirecting, because a browser navigation cannot carry your session header. Owner or admin. |
| `GET /v1/github/repositories` | The repositories the installation was granted. **Session or API key.** |
| `GET /v1/github/repositories/{resource_id}/knowledge-files` | Which of the conventions above the repository already keeps, and whether each already carries an Anona section. **Session or API key.** |
| `POST /v1/github/bindings` | **Session only.** Binds a repository to a space. `knowledge_path` chooses the knowledge file; `null` means none. `knowledge_cadence` is `daily`, `weekly` or `monthly`. `knowledge_corrections` turns sentence corrections on; it needs a `knowledge_path`. |
| `GET /v1/github/bindings/{binding_id}/backfill/preview` | Quotes a history import, both ways: `matched_count` and `estimated_credits` for a window of `target_count` (default 50, maximum 500), and `complete_matched_count` with `complete_estimated_credits` for importing everything. `total_merged` is how many merged pull requests the repository holds. `estimated_hours` and `complete_estimated_hours` say how long each would take, derived from the import rate rather than stated, so they cannot drift from it. Spends nothing. **Session or API key.** |
| `POST /v1/github/bindings/{binding_id}/backfill` | **Session only.** Starts one, once the quote is confirmed. `mode` is `recent` (the default) or `complete`. `expected_credits` must equal the estimate the preview just returned, or `409 backfill_estimate_changed`. Returns `201` and the job. |
| `GET /v1/github/bindings/{binding_id}/backfill` | The repository's most recent import, or `null`. `updated_at` is when it last moved, which is how a stalled import is told from one waiting for its next pass. On a failed import, `error` is the reason in plain words and `retryable` says whether starting again could help. **Session or API key.** |
| `GET /v1/github/bindings/{binding_id}/knowledge-pulls` | Every knowledge pull request opened on this repository, newest first — `open`, `merged` or `closed`, with its number, title, branch and timestamps. The binding's own `knowledge_pr_url` is only the current one. Read live from GitHub, so a pull request you merged or closed reports that immediately. **Session or API key.** |
| `GET /v1/github/answers/{answer_id}` | One answer the audience rule kept out of a pull request thread. |
| `DELETE /v1/github/bindings/{binding_id}` | **Session only.** Unbinds one repository. |
| `DELETE /v1/github/installations/{installation_id}` | **Session only.** Disconnects the installation and removes its bindings. |

Two more are GitHub's to call, not yours: `POST /v1/github/events` receives
every webhook delivery, and `GET /v1/github/callback` is where the browser
lands after an install is approved.

`GET .../knowledge-files` returns `[]` for a repository that keeps none of
them, and `503 github_api_error` when the repository could not be read. Those
are different answers, and a client must not render the second as the first.

`total_merged`, `complete_matched_count` and `complete_estimated_credits` are
`null` together when GitHub would not say how many pull requests the repository
has — its search index is rate limited and can lag. `null` is **unknown**, never
zero: a client must not render it as an empty repository. Counting a window
still works, and `POST ... {"mode": "complete"}` answers
`503 backfill_total_unavailable` until the count comes back.

`mode: "complete"` takes **no** `target_count` — it imports everything that is
left, so a count would mean two different things at once — and sending one is
`422`. Echo `complete_estimated_credits` as `expected_credits` instead. The
quote excludes pull requests an earlier import already took, so a second import
of the same repository is priced on the remainder.

`POST /v1/github/bindings` is a **full replace**. Send `knowledge_path` and
`knowledge_cadence` on every save — omitting the path clears it rather than
leaving it alone, and omitting the cadence resets it to `weekly`.

## Error codes

| Code | Status | Meaning |
| - | - | - |
| `github_knowledge_shared_space` | 403 | A space shared from another organization cannot be written into a repository's knowledge file |
| `github_bad_knowledge_path` | 400 | `knowledge_path` must be a relative path inside the repository, with no `..` segments |
| `github_bad_knowledge_cadence` | 400 | `knowledge_cadence` must be exactly `daily`, `weekly` or `monthly` |
| `github_knowledge_corrections_need_path` | 400 | Corrections were turned on without a `knowledge_path` — there is no file to correct sentences in |
| `backfill_already_running` | 409 | An import is already in progress for this repository |
| `backfill_estimate_changed` | 409 | A pull request merged since the quote; preview again |
| `backfill_binding_read_only` | 409 | The binding is Read only, so an import would capture nothing |
| `backfill_capture_mode_explicit` | 409 | The binding captures only on request, so there is no merge history to import |
| `backfill_key_inactive` | 409 | The binding's API key is revoked or expired, so an import would capture nothing |
| `backfill_total_unavailable` | 503 | GitHub would not say how many merged pull requests the repository has, so importing all of them cannot be quoted. Try again in a minute |
| `insufficient_credits` | 402 | The balance cannot fund the import; nothing was started |
| `github_admin_only` | 403 | Only an owner or admin can connect or disconnect an installation |
| `github_not_connected` | 404 | No GitHub installation is connected for this organization |
| `github_binding_not_found` | 404 | No such binding |
| `github_answer_not_found` | 404 | No such stored answer, or its binding is gone |
| `api_key_not_found` | 404 | `api_key_id` does not name a live key in this organization |
| `space_access_denied` | 403 | The stored answer belongs to a space you cannot reach |
| `space_ambiguous` | 409 | You hold your own space by that name *and* a share of another org's. Bind the qualified `owner_slug:space_name` form |
| `github_api_error` | 503 | GitHub refused or failed the call |
| `github_not_configured` | 503 | GitHub is not configured on this deployment |

Codes are lowercase, in the standard envelope:

```json theme={null}
{ "error": { "code": "github_knowledge_shared_space", "message": "…" } }
```

## Disconnect

**Disconnect** on the GitHub row revokes the installation and removes its
bindings. Your spaces, your memories and anything already merged into a
repository are untouched.

To stop one repository only, unbind it instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.