Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ folded into a version section at release time.
wrapper and its sudo rule, reverse proxy / Cloudflare tunnel /
polling-only ingress with what polling cannot see, smee for a laptop
test, log hygiene), two host recipes in `examples/` with wake policies
sized for GitHub's event volume, and `docs/rayforce-house-rules.md` for
the target repository's practices.
sized for GitHub's event volume, and a house-rules note for the first
target repository's practices (removed after 0.1.0; such notes belong
with the host's deployment).

- **One server.** `src/server.ts` (`GitHubMcplServer`) joins the seven
packages into a single connection, and `src/index.ts` starts it: the MCPL
Expand Down
69 changes: 37 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ or several of one owner; one process per owner.
rollback deletes the comments created after the checkpoint and restores the
bodies of the ones edited after it. Nothing GitHub cannot undo is
checkpointed, and nothing is granted that the host did not grant.
- **No merge, ever.** Not in v1, not behind a flag. The base branch of any
pull request the server opens or retargets is checked against an allow-list.
- **No merge, ever.** There is no merge tool to enable, and no flag that
adds one. The base branch of any pull request the server opens or
retargets is checked against an allow-list.

Built on [`@animalabs/mcpl-core`](../mcpl-core-ts); sibling of
[slack-mcpl](../slack-mcpl) and [telegram-mcpl](../telegram-mcpl).
Built on [`@animalabs/mcpl-core`](https://github.com/anima-research/mcpl-core-ts);
sibling of [slack-mcpl](https://github.com/anima-research/slack-mcpl) and
[telegram-mcpl](https://github.com/anima-research/telegram-mcpl).

> **Status: v1, run end to end against a live GitHub App.** The server answers a
> host's handshake in MCPL or in plain MCP, applies the grant it is given
Expand Down Expand Up @@ -89,9 +91,9 @@ open it in a browser, paste `app-manifest.json` into the field and submit.
</form>
```

Name it on the GitHub page that follows: `rayforce-clone` is the proposal for
the maintainer's clone. The name becomes the bot login with `[bot]` appended
(`rayforce-clone[bot]`), which is what `GITHUB_BOT_LOGIN` must hold.
Name it on the GitHub page that follows. The name becomes the bot login with
`[bot]` appended (an App named `my-github-agent` posts as
`my-github-agent[bot]`), which is what `GITHUB_BOT_LOGIN` must hold.

**b. Catch the code.** GitHub creates the App and redirects your browser to
the manifest's `redirect_url` with `?code=…` — a temporary code, good for one
Expand Down Expand Up @@ -240,9 +242,9 @@ told where Node is instead of looking.
| `GITHUB_WEBHOOK_PATH` | no | `/github/webhook` | Path the listener answers on. |
| `GITHUB_POLL_INTERVAL_MS` | no | `0` (off) | Polling fallback: issues and PRs, issue comments and review comments updated since each list's cursor, plus the checks on the heads of the open PRs seen. `0` is off; a non-zero value below `60000` is refused. Partial — it cannot see a submitted review, a review request or a push ([what polling misses](docs/deploy.md#option-3--polling-only-no-webhook-at-all)) — and it is not meant to run alongside the webhook. |
| `GITHUB_REPOS` | yes | — | Comma-separated `owner/name` allow-list; a write anywhere else is refused by name. Several entries must share one owner (one App installation, one token); a second owner is a second server process. With several, every tool call names its repository. |
| `GITHUB_PR_BASE_ALLOW` | no | empty = no PR target allowed | Branches a pull request may target or be retargeted to. A plain `branch` applies to every repository, `owner/name:branch` to that one: `anima-research/rayforce:dev,main`. |
| `GITHUB_PR_BASE_ALLOW` | no | empty = no PR target allowed | Branches a pull request may target or be retargeted to. A plain `branch` applies to every repository, `owner/name:branch` to that one: `my-org/app:dev,main`. |
| `GITHUB_STATE_FILE` | no | unset = in-memory only | Durable JSON: checkpoint lineage, rollback bookkeeping, and the polling state: a cursor for each repository and list (`pollCursors`), `lastPollAt` where a list with none starts, and the rows announced as new (`pollNew`). A list with a cursor of its own starts from it, not from `lastPollAt`. |
| `GITHUB_BOT_LOGIN` | yes | — | The App's bot login, e.g. `rayforce-clone[bot]`. From-self and mention detection depend on it. |
| `GITHUB_BOT_LOGIN` | yes | — | The App's bot login, e.g. `my-github-agent[bot]`. From-self and mention detection depend on it. |
| `GITHUB_MAINTAINERS` | no | empty | Comma-separated logins marked `isMaintainer` in metadata and tagged `github:actor=maintainer`. |

### 8. Build and run it
Expand Down Expand Up @@ -276,10 +278,12 @@ only, who can read the key, and log hygiene, continue in

### Wake policies are not optional

A rayforce-sized repository produces 60–115 events a day, and one wake costs
roughly 100k input tokens. A policy set that wakes on everything is about
10M tokens a day of ambient chatter. Both example recipes therefore skip by
default and wake only on what is addressed to the agent:
A repository as busy as the one this connector was first built for produces
60–115 events a day (pushes, reviews, inline comments, comments, pull
requests, issues), and one wake costs roughly 100k input tokens. A policy set
that wakes on everything is about 10M tokens a day of ambient chatter. Both
example recipes therefore skip by default and wake only on what is addressed
to the agent:

**The order is the design**, because the host matches first-rule-wins and
`metadataTrue` is an OR-list: one rule that wakes on `isOwnThread` placed
Expand All @@ -297,7 +301,7 @@ exception next, and the wake rules last:
| 6 | `github-new-work` (maintainer clone only) | `github:issue=opened`, `github:pr=opened` | `debounce: 60000` — one wake once the burst settles |
| 7 | `github-ambient` | anything else from `github` | `skip`, and `default` is `skip` too |

`test/wp8-policies.test.ts` runs both recipes through connectome-host's own
`test/policies.test.ts` runs both recipes through connectome-host's own
`EventGate` matcher and asserts that ordering: a green build on the agent's
own pull request is skipped, a broken one wakes it, a bot's pull request
neither wakes nor debounces, a human mention wakes it, and no rule is left
Expand Down Expand Up @@ -340,8 +344,8 @@ Notes worth knowing before you grant them:

- `upsert_sticky_comment(thread, marker, body)` finds the agent's own comment
containing an HTML marker and edits it in place, else creates it. One
running status comment instead of a tail of them — the same thing
`rayforce-audit` does with `<!-- rayforce-audit-failure-comment -->`.
running status comment instead of a tail of them, the pattern CI bots use
with a marker such as `<!-- my-github-agent-status -->`.
- `create_pull_request` and a base change in `edit_pull_request` refuse a base
outside `GITHUB_PR_BASE_ALLOW`. Empty means no pull request may be opened.
- `github.pr` has no merge tool and no branch deletion. There is nothing to
Expand All @@ -353,7 +357,7 @@ Notes worth knowing before you grant them:

Every event carries RFC-001 core tags, the `github:` namespace, and a flat
metadata record the host matches `metadataTrue` against. The per-event detail
lands in `docs/events.md` with work package .4; the vocabulary itself is the
is in [`docs/events.md`](docs/events.md); the vocabulary itself is the
contract and is listed here in full.

**Core tags** (producer `github`):
Expand Down Expand Up @@ -388,7 +392,7 @@ contract and is listed here in full.
| --- | --- |
| `mentioned` | the body mentions the bot |
| `isOwnThread` | the bot authored the thread or has commented in it |
| `isReplyToSelf` | the event replies to a comment the bot wrote: a **review** comment whose `in_reply_to_id` is one of the bot's, or a **top-level** comment that immediately follows one of the bot's on the thread (amendment 2b, turn order). Quoting the bot's text does not count in v1 — see `docs/contract-notes.md`. |
| `isReplyToSelf` | the event replies to a comment the bot wrote: a **review** comment whose `in_reply_to_id` is one of the bot's, or a **top-level** comment that immediately follows one of the bot's on the thread (turn order). Quoting the bot's text does not count in v1 — see `docs/contract-notes.md`. |
| `isReviewRequested` | the bot was asked for a review |
| `isCiFailure` | a check suite on this thread's head failed |
| `fromSelf` | the bot itself acted (**skip this first in every policy set**) |
Expand Down Expand Up @@ -420,8 +424,9 @@ reach metadata and never reach the model.
one agent, one context and one prompt, so keep them to repositories whose
rules the prompt can state together.
- **A base allow-list.** `GITHUB_PR_BASE_ALLOW` is empty by default, which
means no pull request may be opened or retargeted at all. For rayforce it is
`dev`, so the release branch is unreachable even by mistake.
means no pull request may be opened or retargeted at all. Name only the
integration branch (say `dev`) and the release branch is unreachable even
by mistake.
- **No merge.** No tool merges, closes a release branch or deletes a branch.
- **Bodies from strangers are data, not instructions.** An issue body, a
comment, a review and a check's output are text written by other people,
Expand Down Expand Up @@ -465,20 +470,20 @@ Node 22.12 or newer, ESM, TypeScript. Stdout carries only JSON-RPC: every
diagnostic goes through the `Logger`, which writes to stderr. No test in this
package reaches the network.

| File | Work package | Role |
| --- | --- | --- |
| `types.ts` | .1 | Every shared type and constant, the channel and thread id helpers, `readConfig`, `Logger`. Read by all, edited by nobody else. |
| `grant.ts`, `errors.ts` | .1 | Capability-grant state, `uses` validation, the §6.7 degradation receipt, the §6.6 error codes. |
| `github-adapter.ts` | .2 | `GitHubAdapter implements GitHubApi` over octokit: App auth, installation-token refresh, rate-limit backoff. The only file that knows octokit exists. |
| `webhook.ts`, `poller.ts` | .3 | Ingress: signature-checked deliveries and repository since-polls become `NormalizedEvent`s; dedup; allow-list. |
| `tags.ts`, `content.ts` | .4 | Pure functions: `tagsFor`, `renderEvent`, `renderThread`, `renderPullRequestSummary`, `renderChecks`, all bounded by `LIMITS`. |
| `channels.ts` | .5 | Channel registry and descriptors, lazy registration, `channels/incoming`, `channels/publish`, typing as a no-op. |
| `feature-sets.ts`, `tools-*.ts` | .6 | Feature-set declarations and the read, issues, pr and review tools. |
| `tools-comments.ts`, `state.ts` | .7 | Comment tools including the sticky upsert; checkpoint lineage and rollback. |
| `server.ts`, `index.ts` | .9 | Handshake, `featureSets/update`, tool routing, the event pipeline, plain-MCP mode. |
| File | Role |
| --- | --- |
| `types.ts` | Every shared type and constant, the channel and thread id helpers, `readConfig`, `Logger`. Read by all, edited by nobody else. |
| `grant.ts`, `errors.ts` | Capability-grant state, `uses` validation, the §6.7 degradation receipt, the §6.6 error codes. |
| `github-adapter.ts` | `GitHubAdapter implements GitHubApi` over octokit: App auth, installation-token refresh, rate-limit backoff. The only file that knows octokit exists. |
| `webhook.ts`, `poller.ts` | Ingress: signature-checked deliveries and repository since-polls become `NormalizedEvent`s; dedup; allow-list. |
| `tags.ts`, `content.ts` | Pure functions: `tagsFor`, `renderEvent`, `renderThread`, `renderPullRequestSummary`, `renderChecks`, all bounded by `LIMITS`. |
| `channels.ts` | Channel registry and descriptors, lazy registration, `channels/incoming`, `channels/publish`, typing as a no-op. |
| `feature-sets.ts`, `tools-*.ts` | Feature-set declarations and the read, issues, pr and review tools. |
| `tools-comments.ts`, `state.ts` | Comment tools including the sticky upsert; checkpoint lineage and rollback. |
| `server.ts`, `index.ts` | Handshake, `featureSets/update`, tool routing, the event pipeline, plain-MCP mode. |

Changelog entries land as fragments in [`changelog.d/`](changelog.d/README.md).
Contract decisions taken where TASK-SRHKG.1's contract left a type
Design decisions taken where the original contract left a type
under-specified are in [`docs/contract-notes.md`](docs/contract-notes.md). A
target repository's own operating notes — branch model, commit style, CI
gates, review rounds, issue style — belong with the host's deployment, beside
Expand Down
2 changes: 1 addition & 1 deletion app-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "rayforce-clone",
"name": "my-github-agent",
"description": "An agent living in one GitHub repository: it wakes on issues, comments, pull requests, reviews and check results, and answers in the thread it was spoken to in. Runs as github-mcpl, an MCPL connector. No merge, no release-branch write.",
"url": "https://example.com/github-mcpl",
"public": false,
Expand Down
2 changes: 1 addition & 1 deletion changelog.d/multi-repo.changed.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
are required instead of defaulting, so a call can never land in the wrong
one.
- `GITHUB_PR_BASE_ALLOW` entries can be scoped: `branch` applies to every
repository, `owner/name:branch` to that one (`anima-research/rayforce:dev,main`).
repository, `owner/name:branch` to that one (`my-org/app:dev,main`).
A scoped entry naming a repository outside `GITHUB_REPOS` is refused at
startup; a refusal names the branches allowed on that repository.
10 changes: 10 additions & 0 deletions changelog.d/neutral-docs.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- The documentation, the App manifest and the examples describe a general
GitHub connector for any MCPL host: the name of the first target
repository, internal task identifiers and work-package references are gone
from the README, `docs/` and the source and test file headers, apart from
the branch and marker names the end-to-end report's transcript records.
The App's proposed name is `my-github-agent` in the manifest and in the
README's examples, and the links to the sibling packages point at their
GitHub repositories. Test data uses neutral repository and bot names; the
two documentation tests are `test/docs.test.ts` and
`test/policies.test.ts`.
4 changes: 2 additions & 2 deletions changelog.d/recipes-by-role.changed.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
- The example recipes are named by role, `examples/recipe-maintainer-clone.json`
and `examples/recipe-contributor.json`, with a neutral agent name, `UTC`
and a prompt that names no repository; the wake policies and their tests
are unchanged. The rayforce house rules left this repository: a target
repository's operating notes belong with the host's deployment, beside its
are unchanged. A target repository's house rules left this repository:
its operating notes belong with the host's deployment, beside its
recipe, since the connector enforces none of them.
Loading
Loading