@chamvi/notify-github
The GitHub issue comment notifier. The public API of packages/notify-github/src/index.ts.
npm install @chamvi/notify-github @chamvi/github @chamvi/core --save@chamvi/core and @chamvi/github are peer dependencies. The App needs Issues read and write. See GitHub App for the App setup.
githubNotifier
function githubNotifier(options: GithubNotifierOptions): INotifier;
const GITHUB_NOTIFIER_TIMEOUT_MS: 30_000;
const COMMENT_BODY_MAX_LENGTH: 60_000;/** The issue or pull request comments go to for runs not started by one. */
export type GithubNotifierTarget = {
/** `owner/name`. */
repo: string;
/** The issue or pull request number. */
number: number;
/** The installation on `repo`, for a repository the run's installation does not cover. */
installationId?: number;
};
export type GithubNotifierOptions = {
app: IGithubApp;
/** Default `'github'`. */
name?: string;
/** Logins mentioned at the top of every comment (without `@`). */
mention?: string | readonly string[];
/** Where to comment for runs without an issue or pull request (cron, http, a push delivery). */
target?: GithubNotifierTarget;
/**
* A link to the approval in the chamvi UI, e.g.
* `https://<app>/ui/approvals?run=<runId>&approval=<approvalId>`, where the
* signed-in owner answers it; it carries no token.
*/
approvalUrl?: (ref: {
runId: string;
approvalId: string;
agent: string
}) => string;
/** Default `GITHUB_NOTIFIER_TIMEOUT_MS` (30 s). */
timeoutMs?: number;
clock?: IClock;
};Throws InvalidAgentDefinitionError for a missing app, an empty name, a mention that is no GitHub login (without @), a target.repo that is not owner/name (REPO_PATTERN of @chamvi/github), a target.number, target.installationId or timeoutMs that is no positive integer and an approvalUrl that is no function.
const gh = createGithubApp({
appId: process.env.GITHUB_APP_ID ?? '',
privateKey: process.env.GITHUB_PRIVATE_KEY ?? '',
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET ?? '',
slug: 'octo-triage',
});
const triage = defineAgent({
name: 'triage',
model,
instructions: 'Close duplicate issues.',
triggers: [githubWebhook({ events: ['issues.opened'] })],
tools: () => gh.writeTools(),
approvals: { github_close_issue: 'user' },
// the approval request becomes a comment on the run's issue
notify: 'github',
});
const engine = new Engine({
store: new MemoryStore(),
agents: [triage],
notifiers: {
github: githubNotifier({
app: gh,
// mentioned at the top of every comment
mention: 'octocat',
// for runs not started by an issue or pull request event
target: { repo: 'octo/ops', number: 1 },
// optional: a link to the approval in the UI, without the token
approvalUrl: ({ runId, approvalId }) => `https://chamvi.example/ui/approvals?run=${encodeURIComponent(runId)}&approval=${encodeURIComponent(approvalId)}`,
}),
},
resumeSecret: process.env.RESUME_SECRET ?? '',
});Target and installation
| Run | Comments on | As installation |
|---|---|---|
source: 'github' with repository.full_name and issue.number or pull_request.number | that issue or pull request | the event's installationId, else the app's |
any other github run (e.g. push) | target | target.installationId, else the event's installationId, else the app's |
| cron, http, mcp, webhook | target | target.installationId, else the app's installationId |
Set target.installationId when target lies in a repository of another installation than the runs' (an App installed on several organizations).
Without an issue and without target, or without an installation, notify() throws InvalidAgentDefinitionError before any request, on every poll: set target for agents not started by issue or pull request events.
The comment
@<mention> @<mention>
### <title, whitespace runs collapsed to one space>
<fence>
<body, cut to COMMENT_BODY_MAX_LENGTH characters>
<fence>
Waiting for approval until <expiresAt ISO>. Approve or deny: <approvalUrl({ runId, approvalId, agent })>
Run `<runId>` of agent `<agent>`.
<!-- chamvi:effect=<idempotencyKey> -->The fence is one backtick longer than the longest backtick run in the body (at least three; runs over 31 backticks are broken with U+200B zero-width spaces, so such a body is not byte-identical to the tool input; without it a body of backticks would need a fence as long as itself and push the comment over GitHub's 65 536 limit), so mentions and issue references inside notify nobody and no input closes it. The title has every run of whitespace (line breaks included) collapsed to one space and is trimmed, so it stays one heading line. Without approvalUrl (or for a notification without an approvalId) the approval line ends Answer it in the chamvi UI or through POST /runs/:id/approve.. approvalUrl gets the run id, the approval id and the agent, never the token: the resume token never appears in a comment or a request, and the owner answers in the UI, signed in, through POST /runs/:id/approve. A key that is not ^[\w:.-]{1,255}$ or contains -- goes into the marker as sha256:<hex>.
Dedupe and failures
Before posting, the notifier pages the issue's comments (since = the run's receivedAt minus one hour, 100 per page) and resolves without posting when one by the App bot (IGithubApp.botLogin(), compared case-insensitively) holds the marker; a copy by anyone else does not count. Every failure throws a ChamviError with code github_request (GitHub <status>: <message>, GitHub request failed: <message>); a list or post that takes longer than timeoutMs throws GitHub did not answer within <ms>ms. The token exchange and GET /app are not bound by timeoutMs; @chamvi/github aborts each after 30 s itself.
Context
function provideGithubNotifierContext(provider: IContextProvider, notifier: INotifier): void; // key 'notify-github:<notifier.name>'
function injectGithubNotifierContext(ctx: ToolContext, name?: string): INotifier; // name default 'github'One key per notifier name, so two GitHub notifiers of different names are both provided; injecting a name never provided throws ContextNotProvidedError naming notify-github:<name>. githubNotifierModule() provides its notifier; see Context.
Module
// @chamvi/notify-github/module, needs @chamvi/app
function githubNotifierModule(options: GithubNotifierModuleOptions): IModule; // name 'chamvi:notifier:<name>', default name 'github'Contributes githubNotifier() to the app's notifiers, with the dashboard approval link from CHAMVI_PUBLIC_URL unless approvalUrl is given, on the app's clock, and provides it on the engine in its engineCreated hook. options.app is the IGithubApp (githubModule().app). @chamvi/app is an optional peer: the main entry never imports it.
/*
* Copyright (c) 2026.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/
import type { GithubNotifierOptions } from '../notifier/types.ts';
/** `githubNotifier()` options; `approvalUrl` defaults to the UI link from `CHAMVI_PUBLIC_URL`, `clock` is the app's. */
export type GithubNotifierModuleOptions = Omit<GithubNotifierOptions, 'clock'>;