Repository navigation
v0.3.1
v0.3.1 lets a run start runs of its own agent, lets an approval offer options, and gives
@chamvi/githubthe tools to work pull requests. It covers everything since v0.3.0.
📣 Some News
Every package is at 0.3.1 again
v0.3.0 left @chamvi/http, channel-cron, channel-openclaw and tool-web at 0.2.2. This release puts all sixteen packages back on one version, 0.3.1, so you can pin ^0.3.1 everywhere. The four packages that skip 0.3.0 gain nothing breaking on the way.
👀 Highlights
🤝 Hand-offs: a run starts runs of its own agent
A tool can start a new run of its own agent with startHandoff(ctx, { payload, idempotencyKey?, concurrencyKey?, address? }). It is fire and forget: the parent goes on at once, and the tool gets { runId, deduplicated } back.
This helps when one run would otherwise do everything under one budget. A daily sweep that finds ten pull requests needing work can hand off one run per pull request instead. Each child gets its own budget, its own approvals and its own concurrency key. An agent opts in with handoff.on() on the new agent channel:
const handOffPr = defineTool({
description: 'Starts a run of its own for one pull request that needs work.',
inputSchema: z.object({ pr: z.string().describe('owner/repo#number') }),
execute: async ({ pr }, { context }) => {
// the day the daily run was fired, not the wall clock: a redo keeps the key
const day = context.run.trigger.receivedAt.toISOString().slice(0, 10);
return startHandoff(context, {
payload: { pr },
idempotencyKey: `${pr}@${day}`,
address: pr,
});
},
});
const maintainer = defineAgent({
name: 'maintainer',
instructions: 'Every morning, hand off each pull request that needs work.',
channels: [
cron.on('0 6 * * *'),
// the opt-in: runs of this agent may start runs of this agent
handoff.on({
schema: z.object({ pr: z.string() }),
concurrencyKey: (p) => p.pr,
maxPerRun: 20,
}),
],
tools: { hand_off_pr: handOffPr },
});The limits hold across crashes, approval pauses and tool code that misbehaves:
- A child inherits the parent's invoker snapshot, so its grants never widen.
- The depth is 1: a handed-off run cannot hand off again.
maxPerRun(default 100) is a durable count.- A redone call returns the same child run.
- A given
idempotencyKeydedupes across runs, so tomorrow's sweep does not start a second run for the same pull request on the same day.
The dashboard's run page links a child to its parent.
Note
@chamvi/app now registers a channel named agent. A channel of your own with that name fails the app setup with the duplicate-channel error. On a bare Engine, add agentChannel() to channels yourself.
🗳️ Approvals with options
approve() can now ask the owner to choose instead of just answering yes or no. Pass options (up to 20, each with an id, a label and an optional detail) and multiple: true for a multiple choice. The decision then carries selected: the chosen ids, in the order of your options.
const pickRules = defineTool({
description: 'Asks which new lint rules to adopt.',
inputSchema: z.object({}),
execute: async (_input, { context }) => {
const decision = await approve(context, {
title: 'Adopt which new rules?',
body: 'Each option shows what the rule changes.',
options: [
{ id: 'unicorn/no-null', label: 'unicorn/no-null', detail: '-const user = null;\n+const user = undefined;' },
{ id: 'unicorn/prefer-at', label: 'unicorn/prefer-at', detail: '-items[items.length - 1]\n+items.at(-1)' },
],
multiple: true,
});
if (!decision.approved) return `Adopt none: ${decision.reason ?? 'denied'}.`;
return `Adopt ${decision.selected?.join(', ')}.`;
},
});Every place that shows an approval also shows its options:
- Dashboard: renders a radio or checkbox per option, with its
detailin a collapsible block (a diff is colored by line). Approve stays disabled until the selection is valid. - Console, GitHub and OpenClaw channels: list
<id>: <label>and link to the dashboard. They never send the detail. - REST and MCP:
POST /runs/:id/approveandapprove_runtakeselected. A selection that does not fit the wait point is a 400validation_error.
Note
On resume, approve() checks the recorded answer against the options the tool passes now. If they no longer fit (an id dropped, a multiple choice turned single), it returns a denial with the reason options changed, so your tool never gets an id it did not offer. A plain approve() without options works exactly as before.
🐙 Pull request tools for @chamvi/github
@chamvi/github now has the tools a maintainer agent needs to work through pull requests, and one App can serve several accounts from one run.
- Per-repository installations: each tool acts as the installation of the repository it names. It looks that installation up with
GET /repos/{owner}/{repo}/installationand caches it for 10 minutes, so one App installed on several accounts and organisations reaches all of them.IGithubApp.repositoryInstallation()is public. - New read tools:
github_list_repositories,github_list_issues,github_list_pull_requestsandgithub_get_pull_request_checks(check runs plus commit statuses, with the required contexts from branch protection and rulesets). There is alsogithub_get_push_target, which says whether the App can push to a pull request's head branch.github_get_pull_requestnow also returns the head repository, fork status, mergeability and labels. - New effect tools:
github_create_issue,github_create_pull_requestandgithub_merge_pull_request. The first two find their earlier result by its effect marker, so a redone step creates nothing twice. selfEventsongithub.on(): events the App's own bot may start a subscription with, such as the pull requests the agent opened itself.issue_comment,issuesandpull_request_review_commentare refused, so the bot's own comment never starts the agent again.
github.on(['pull_request.opened', 'check_suite.completed'], {
// the agent's own pull requests start it too
selfEvents: ['pull_request.opened', 'check_suite.completed'],
}),
// ...
tools: () => ({ ...gh.readTools(), ...gh.writeTools({ mergeMethod: 'squash' }) }),github_merge_pull_request takes the sha the agent reviewed and re-reads the pull request before it merges. It refuses with ToolDeniedError in any of these cases:
- the head moved off
sha - the pull request is a draft, not mergeable or blocked
- the required checks cannot be read
- CI has reported no check yet
- any check is still pending
- a required check failed
A failing check that is not required still merges (unstable).
Warning
Never put the App on a ruleset bypass list or a branch protection exception. GitHub reports a blocked pull request as clean to a token that may bypass, so protection would no longer stand between the model and the default branch.
Tip
Name the actions in selfEvents. A bare pull_request also takes the labeled, closed and synchronize events that the agent's own labels, merges and pushes cause, so every such write starts the agent again.
📖 GitHub App tools · @chamvi/github reference
⚠️ Heads-Up Before Upgrading
| Change | What to do |
|---|---|
@chamvi/app registers a channel named agent |
Rename any channel of your own called agent |
A GitHub tool call that finds no installation is now a github_request tool error, not a FatalToolError |
Do not rely on that call failing the run. A public repository the App is not installed on is now readable through the fallback installation |
| The new pull request tools need more App permissions | Grant Pull requests write and Checks and Commit statuses read where an agent uses them |
⬆️ Upgrading
npm install @chamvi/app@^0.3.1 @chamvi/cli@^0.3.1No store migration is needed. Hand-off lineage and approval options live in existing jsonb columns. Bump every other @chamvi/* package you use to ^0.3.1.
👉 Changelog
Features
- core: start runs of the same agent from a tool (#64) (083c385)
- core: approvals with options (#65) (994da69)
- github: pull request tools and per-repository installations (#66) (1a87a3a)