@kunchenguid/m87
v0.1.9
Published
Local-first review queue (event-driven)
Readme
Your issues and pull requests pile up faster than you can read them. You could hand the whole thing to an agent, but then it is commenting, closing, and merging on your behalf while you are not looking - and that is exactly the part you do not want to give away.
m87 splits the work.
A local daemon syncs your sources, an AI agent reads each item and recommends what to do, and the recommendation sits in a queue.
Nothing source-visible happens until you review the exact outgoing action and explicitly approve it.
- Local-first - the queue, daemon, SQLite database, and ACP sessions all live under
~/.m87. No hosted backend. - Preview-then-approve - the agent only recommends.
Every external write waits behind a preview and explicit approval; CLI approval uses
--confirm, and destructive actions need--confirm-destructive. - Pluggable sources - GitHub issues and PRs out of the box, plus a documented plugin contract for adding trusted sources of your own.
Quick Start
Run the guided setup in a terminal:
$ m87 initThe wizard creates local state, lets you use auto-detect or pick a detected AI agent, connects GitHub or skips source setup, and finishes by choosing whether M87 runs in the background at login, for this session only, or later. If M87 is already running, that final step says so and changes the choice to keeping it running at login, keeping it running for this session only, or stopping it. For scripts or CI, use flags instead of prompts:
$ m87 init --yes \
--agent auto \
--plugin github \
--github-repo <owner>/<repo>
$ m87 sync
$ m87External writes still wait behind preview and approval when using the CLI:
$ m87 preview <recommendation-id>
$ m87 approve <recommendation-id> --confirmRun m87 with no arguments in a terminal to open the live interactive inbox instead.
Use ↑/↓ to move between items, press 1-9 to select an option, review the WILL DO detail, then press a to approve the selected option.
Use j/k to scroll long WILL DO details.
Press i for queue details, startup help, and other inbox info; press i or Esc to return.
Install
npm (global)
npm install -g @kunchenguid/m87
m87 --versionFrom source
git clone https://github.com/kunchenguid/m87
cd m87
npm install -g . # builds dist/ via prepack, then installs the `m87` binaryTo hack on the code without installing, run it straight from source with node src/cli/index.js <command> (see Development).
How It Works
The daemon is the only worker. It owns sync, triage, action execution, and automation jobs - the CLI and TUI just emit intents and read state.
sources (github, plugins, ...)
│ daemon sync
▼
items ───────► agent triage (ACP) ───────► recommendation
│
▼
inbox / list
│
preview / WILL DO ◄─┘ (the gate)
│
explicit approval
(TUI `a` or CLI `--confirm`)
│
┌─────────────────┴─────────────────┐
▼ ▼
source-visible action automation job (draft PR)
│ │
▼ ▼
audit trail reviewable PR- The daemon is the sole actor - syncing, triage, and writes all flow through one background process so there is a single source of truth and one audit trail.
- Approval is preview-then-approve - the CLI
previewcommand and the TUI WILL DO detail render the precise effect before a human approval reaches a source. - Agent is ACP-pluggable -
m87auto-detects an installed provider CLI (claude, thencodex, thenopencode) as itsacp:target, or you set one explicitly in config. - Automation jobs stay reviewable - approving a fix option queues a coding-agent job that the daemon runs into a draft pull request.
It never merges for you.
When the PR opens asynchronously (the no-mistakes path), the daemon re-probes for it on a capped backoff until it appears, so the job settles without a manual
m87 job attach. - Triage sees running automation - a re-triage receives the item's open jobs, executed actions, and prior approval as
local_automation_state, so it recommends waiting for or building on in-flight work instead of duplicating it.
CLI Reference
| Command | Description |
| --------------------------------- | ------------------------------------------------------------ |
| m87 init | Open guided setup on a TTY, or initialize local state |
| m87 status | Show resolved agent, plugins, queue, and inbox status |
| m87 sync | Nudge the daemon to sync + triage all active plugins now |
| m87 list | List the active review inbox |
| m87 view <item> | Show one item and its recommendation detail |
| m87 open <item> | Print the item's source URL |
| m87 copy-handoff <item> | Print a copyable agent handoff prompt for one item |
| m87 preview <rec> | Preview what approving an option would do (the gate) |
| m87 approve <rec> | Approve an option - the one human gate |
| m87 triage <item> | Triage one newly synced item |
| m87 rerun <item> | Supersede the recommendation and re-triage an item |
| m87 dismiss <item> | Dismiss an item |
| m87 mark-handled <item> | Mark an item handled |
| m87 snooze <item> <dur> | Snooze an item until later (e.g. 1d, 4h) |
| m87 plugin ... | add, list, configure, sync, doctor source plugins |
| m87 job ... | list, view, attach automation jobs |
| m87 daemon ... | start, stop, status, restart, install, uninstall |
| m87 audit export | Export the action audit trail |
| m87 audit receipt <id> | Show a receipt for an approval |
| m87 state export\|import | Portable, secret-redacted state export/import |
| m87 retention policy | Show local data retention settings |
| m87 retention set <field> <ttl> | Change a retention setting |
| m87 retention cleanup | Purge data expired by the retention policy |
| m87 update [--check] | Check for or install a newer npm release |
Flags
| Command | Flag | Description |
| -------------------------- | ----------------------------- | ----------------------------------------------------- |
| init | --yes | Apply setup defaults without prompts |
| init | --wizard | Force the interactive setup wizard |
| init | --agent <target> | auto or an explicit acp:<target> |
| init | --plugin github\|skip\|none | Configure GitHub or skip source setup |
| init | --github-repo <repo...> | Sync explicit owner/repo sources |
| init | --github-username <login> | GitHub login for discovered scopes |
| init | --github-owned | Sync repositories owned by the GitHub user |
| init | --github-public-owned | Sync public repositories owned by the user |
| init | --github-public-starred | Sync public owned repositories starred by user |
| init | --github-authored-external | Sync authored issues and PRs outside configured repos |
| init | --install-service | Start now and launch at login |
| init | --no-install-service | Do not install the login service |
| init | --start-daemon | Start now for this session only |
| preview | --option <selector> | Pick an option by id or position |
| approve | --option <selector> | Pick an option by id or position |
| approve | --confirm | Confirm external-write actions |
| approve | --confirm-destructive | Confirm destructive actions |
| rerun | --instructions <text> | Extra instructions for the agent |
| plugin add / configure | --config <k=v...> | Set plugin configuration pairs |
| daemon run | --once | Process the queue once and exit |
| update | --check | Only check the registry; never install |
Sources
GitHub
The bundled GitHub plugin syncs issues and pull requests through gh, and supports comments, close/reopen, PR reviews, and merges.
gh auth status || gh auth login
m87 init --yes \
--plugin github \
--github-repo <owner>/<repo>Manual plugin setup is still available:
m87 plugin add github
m87 plugin configure github \
--config username=<github-login> \
--config explicit_repos=<owner>/<repo>
m87 plugin doctor # confirm the daemon resolves your gh credentialsgh must be authenticated in the same environment the daemon runs under.
Configure at least one source (explicit_repos, owned_repos=true, repo_conditions, or authored_external=true), or sync completes with an empty inbox.
Every item is stamped with a role: maintainer items (repos you own or configure) expose all actions including merge and review; contributor items (things you authored elsewhere, via authored_external) carry a [contrib] badge and only offer comment/close.
Common GitHub plugin config keys:
| Key | Meaning |
| --------------------- | ------------------------------------------------------------------------------------------------------ |
| username | GitHub login to use when resolving owned repos and authored external work. |
| explicit_repos | Comma-separated owner/repo list to sync. |
| owned_repos | true to sync repositories owned by username. |
| repo_conditions | Comma-separated discovery filters: all_owned, all_public_owned, or all_public_owned_and_starred. |
| authored_external | true to sync issues and PRs authored by username outside configured repositories. |
| exclude_repos | Comma-separated owner/repo list to skip. |
| max_repos | Maximum repositories to sync when discovering repos. |
| sync_limit_per_repo | Maximum issues or pull requests to fetch per repository. |
| lookback_days | Activity lookback window in days. |
| activity_probe | true to probe extra activity when selecting work. |
| fix_pr_create | How maintainer fix jobs submit review work: auto, no-mistakes, gh, or disabled. |
| fix_contrib_push | How contributor fix jobs leave the workspace: auto, no-mistakes, or disabled. |
Gmail
The bundled Gmail plugin is demo-only and fixture-backed in this release. It does not perform live Gmail writes.
Configuration
Config lives at ~/.m87/config.yaml by default.
Set M87_STATE_DIR to change where the SQLite database, plugin state, ACP sessions, daemon PID, daemon log, and retained artifacts are stored.
agent: null # auto-detect a provider CLI (claude, then codex, then opencode); or set an acp: target
poll_interval: 300
acp_registry_overrides: {}
plugins: {}If ~/.m87/AGENTS.md exists, its contents are passed to every triage as a user policy, so you can steer recommendations globally.
Run m87 status to see the resolved agent.
Running As A Service
m87 daemon run # foreground; logs every sync/triage/warn until Ctrl-C
m87 daemon start # detached background process
m87 daemon status # report running, stale, and installed versions
m87 daemon install # managed OS service: launchd / systemd --user / schtasks
m87 daemon uninstallA detached or managed daemon writes operational logs to ~/.m87/daemon.log, including startup, shutdown, loop errors, sync failures, and sync recovery.
Installing the managed service stops any session daemon first, then lets the service own the background process so only one daemon works on the local state.
If m87 daemon restart cannot stop the old daemon within its stop window, it reports stop_failed instead of starting another daemon.
When the managed service is installed, m87 daemon restart bounces the daemon through the service manager, since the manager respawns any daemon that exits on its own.
Failed source syncs are retried with backoff instead of being parked forever; a plugin returns to active after a later successful sync.
A running daemon keeps executing the code it loaded at startup, so a package upgrade alone would leave it on stale code.
The daemon therefore watches the installed package.json (once a minute) and, when the version on disk no longer matches the one in memory, drains and restarts itself onto the new code: it schedules no new work, lets in-flight agent turns finish (bounded), and then respawns - through the service manager when the managed service is installed, as a fresh detached process otherwise.
Durable queued events are simply picked up by the upgraded daemon, so nothing is lost across the restart.
m87 update restarts the daemon immediately after a successful install without waiting for that detection.
m87 update --check only reports whether an update is available and never changes the daemon.
m87 daemon status compares the daemon's reported version against the installed CLI and reports running_stale with a restart hint when they differ - normally a transient state that the self-restart resolves within a minute or two.
A managed daemon launched from a GUI context inherits a minimal PATH, so m87 resolves your login-shell environment at startup to find gh, git, and provider CLIs.
Set M87_SKIP_SHELLENV=1 to disable that resolution.
Development
pnpm install
pnpm run build # bundle src/ -> dist/cli.js via esbuild
pnpm run lint # eslint
pnpm run typecheck # tsc --noEmit
pnpm test # vitest
node src/cli/index.js <command> # run from source, no build neededEnd-to-end tests run the source CLI in tracked process groups and sweep any stranded CLI or plugin subprocesses after the run.
Contributions to main must be pushed through no-mistakes - see CONTRIBUTING.md.
