Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions dev/changelog/add-mngr-donate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Registered the new `libs/mngr_donate` workspace member (root `pyproject.toml` coverage list + `uv.lock`).
1 change: 1 addition & 0 deletions libs/mngr/changelog/add-mngr-donate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Registered the new `imbue-mngr-donate` plugin (`mngr donate`) in the plugin catalog as a default-installable DEPENDENT gated on `imbue-mngr-usage`.
38 changes: 38 additions & 0 deletions libs/mngr/docs/commands/secondary/donate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!-- This file is auto-generated. Do not edit directly. -->
<!-- To modify, edit the command's help metadata and run: uv run python scripts/make_cli_docs.py -->

# mngr donate
**Usage:**

```text
mngr donate [OPTIONS]
```
**Options:**

## Common

| Name | Type | Description | Default |
| ---- | ---- | ----------- | ------- |
| `--format` | text | Output format (human, json, jsonl, FORMAT): Output format for results. When a template is provided, fields use standard python templating like 'name: {agent.name}' See below for available fields. | `human` |
| `-q`, `--quiet` | boolean | Suppress all console output | `False` |
| `-v`, `--verbose` | integer range | Increase verbosity (default: BUILD); -v for DEBUG, -vv for TRACE | `0` |
| `--log-file` | path | Path to log file (overrides default ~/.mngr/events/logs/<timestamp>-<pid>.json) | None |
| `--log-commands`, `--no-log-commands` | boolean | Log commands that were executed | None |
| `--headless` | boolean | Disable all interactive behavior (prompts, TUI, editor). Also settable via MNGR_HEADLESS env var or 'headless' config key. | `False` |
| `--safe` | boolean | Always query all providers during discovery (disable event-stream optimization). Use this when interfacing with mngr from multiple machines. | `False` |
| `--plugin`, `--enable-plugin` | text | Enable a plugin [repeatable] | None |
| `--disable-plugin` | text | Disable a plugin [repeatable] | None |
| `-S`, `--setting` | text | Override a config setting for this invocation (KEY=VALUE, dot-separated paths; append __extend to the leaf key to extend list/dict/set fields) [repeatable] | None |

## Other Options

| Name | Type | Description | Default |
| ---- | ---- | ----------- | ------- |
| `--skill` | text | Donation skill to run (the subdir name under the host-dir skill cache). | `document-review` |
| `--skill-repo` | text | Upstream git repo the skill (code + prompts) is checked out from. | `https://gitlab.com/sinnott-armstrong-lab/elsi-checklist/credits-for-science/document-review-skill.git` |
| `--skill-ref` | text | Git ref to check out: a branch to track, or a pinned commit for a reviewed version. | `c2e9bbe799c20c9da3896c2205991164f10555fd` |
| `--agent-name` | text | Name for the created donation agent. | `donate-extra-quota-bio` |
| `--dry-run` | boolean | Report the spare-capacity decision without creating an agent. | `False` |
| `--start` | boolean | Schedule donate to run automatically (installs a launchd LaunchAgent; macOS only) and exit. | `False` |
| `--stop` | boolean | Remove the scheduled donate LaunchAgent and exit. | `False` |
| `--interval-minutes` | integer range | With --start: how often the scheduled donate runs. | `10` |
8 changes: 8 additions & 0 deletions libs/mngr/imbue/mngr/plugin_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,14 @@ class CatalogEntry(FrozenModel):
is_recommended=True,
gate=RequiredPackagesGate(packages=("imbue-mngr-pi-coding", "imbue-mngr-usage")),
),
CatalogEntry(
entry_point_name="donate",
package_name="imbue-mngr-donate",
description="Spend spare Claude capacity on a donation skill (`mngr donate`)",
tier=PluginTier.DEPENDENT,
is_recommended=True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is fairly experimental, I'd probably keep is_recommended False by default for now

Once we're ready to release it (and after we've done some testing, have some initial science partners, etc), then we can flip it to True

gate=RequiredPackagesGate(packages=("imbue-mngr-usage",)),
),
# --- INDEPENDENT, no signal ---
CatalogEntry(
entry_point_name="usage",
Expand Down
11 changes: 11 additions & 0 deletions libs/mngr_donate/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog - mngr_donate

A concise, human-friendly summary of changes for the `mngr_donate` library. Entries are categorized using the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories: Added, Changed, Deprecated, Removed, Fixed, Security.

For the full, unedited changelog entries, see [UNABRIDGED_CHANGELOG.md](UNABRIDGED_CHANGELOG.md).

## [Unreleased]

### Added

- New `imbue-mngr-donate` plugin, extracted from `imbue-mngr-usage`: provides `mngr donate`, which spends spare Claude capacity on a donation skill (default: scientific `document-review`). Depends on `imbue-mngr-usage` for the spare-capacity snapshot.
98 changes: 98 additions & 0 deletions libs/mngr_donate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# imbue-mngr-donate

`mngr donate` -- spend spare Claude capacity on a donation skill instead of letting
idle quota expire.

One invocation is a single tick: read the account-level usage snapshot (the same one
`mngr usage` shows), and *if* there's spare capacity (5h window under budget **and** the
week under its pace line), launch a headless Claude agent that runs a donation skill
(default: scientific `document-review`) to completion, then auto-cleans up. When there's
no spare capacity -- or no usage data to judge from -- it does nothing and says so.

This is a separate plugin from `imbue-mngr-usage` on purpose (measuring usage and donating
spare quota are orthogonal), but it depends on it: installing `imbue-mngr-donate` pulls in
`imbue-mngr-usage`, whose snapshot API + `usage` plugin config donate reads at runtime.

## Run a donation

```bash
# From inside a trusted git repo (the agent is sourced from the current dir):
mngr donate # one tick: donate now if there's spare capacity
mngr donate --dry-run # show the decision + numbers, launch nothing
mngr donate --skill my-skill # run a different skill (default: document-review)
```

## Schedule it (drain spare capacity over time)

A single tick spends at most one skill run's worth of quota. To actually *drain* spare
capacity, schedule it -- the schedule, not any one tick, is what uses up the idle quota:

```bash
mngr donate --start # install a launchd LaunchAgent (every 10 min by default)
mngr donate --start --interval-minutes 5
mngr donate --stop # remove it
```

`--start`/`--stop` are **macOS-only** and install a **launchd LaunchAgent**
(`com.imbue.mngr.donate` in `~/Library/LaunchAgents/`). launchd -- not cron -- because the
agent must run inside your **login session** to reach the macOS keychain where Claude's
subscription token lives; a cron job runs outside it and every tick fails `Not logged in`.
launchd also catches up after sleep. On other platforms, schedule `mngr donate` yourself.

## Authentication (read this when ticks start failing with 401)

The donation agent is **headless**, and headless Claude authenticates differently from the
Claude app you use interactively -- this difference will bite you:

- The desktop app's login popup (`/login`) renews **only the app's own session**. It does
not refresh the credentials headless `claude` reads.
- A plain web login yields a **short-lived (~8h) access token with no refresh token**. Any
scheduled donate setup that leans on it works for a few hours, then every tick fails with
`Failed to authenticate. API Error: 401 Invalid authentication credentials` -- typically
overnight, which makes it look like the schedule "randomly broke."

The fix is a **long-lived (~1 year) token** minted for exactly this use case. One-time setup:

```bash
# 1. Mint it (same login popup, but prints a long sk-ant-oat... token in the terminal --
# copy ALL of it; it usually wraps across lines):
claude setup-token

# 2. Stash it in the macOS keychain (prompts for a "password" -- paste the token there,
# so it never lands in a file or shell history). Add -U to overwrite an existing entry:
security add-generic-password -s mngr-donate-oauth -a "$USER" -w
```

At each tick, donate looks for that `mngr-donate-oauth` keychain entry and exports it to the
agent as `CLAUDE_CODE_OAUTH_TOKEN` (which takes precedence over the session token). If
`CLAUDE_CODE_OAUTH_TOKEN` is already set in the environment, or the entry doesn't exist, the
environment is inherited unchanged -- so without the stash, donate works exactly as long as
your current session token does.

## Skills: pinned code, dynamic prompts

The donation skill (its code **and** prompts) lives in the lab's own upstream git repo --
the single source of truth. `mngr donate` **checks it out** into a host-dir cache
(`<host_dir>/donate-skills/<skill>/`) and points the agent at it, so the lab can revise the
skill without an mngr release:

- `--skill-repo` — the upstream repo (default: the `document-review` skill's GitLab repo).
- `--skill-ref` — the git ref to check out. A **branch** to *track* (each tick adopts the
latest — good for a fast-moving lab), or a **pinned commit** for a reviewed, reproducible
version that imbue bumps deliberately.

**Pin the ref for anything unattended.** The donation agent runs with
`--dangerously-skip-permissions`, so whatever code is at `--skill-ref` executes on your
machine. Tracking a branch auto-adopts the lab's changes; pinning a reviewed commit is the
safe default for scheduled runs (bump the ref to adopt updates after review).

## Notes

- **Run it from a trusted git repo.** The donation agent is created from the current
directory; `--start` bakes that directory (and your `PATH`) into the LaunchAgent.
- **It needs usage data.** Spare capacity is judged from the account-level snapshot, which is
populated by mngr-managed Claude agents. With none recorded recently, `donate` reports
"can't tell" and skips rather than guessing.
- **Logs.** Each run's full event stream is tee'd to `<host_dir>/donate-logs/<agent>-<ts>.jsonl`,
and scheduled runs also append to `<host_dir>/donate-logs/schedule.log`, so a run survives the
agent's auto-destroy for later inspection.
5 changes: 5 additions & 0 deletions libs/mngr_donate/UNABRIDGED_CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Unabridged Changelog - mngr_donate

Full, unedited changelog entries consolidated nightly from individual files in `libs/mngr_donate/changelog/`.

For a concise summary, see [CHANGELOG.md](CHANGELOG.md).
Empty file.
1 change: 1 addition & 0 deletions libs/mngr_donate/changelog/add-mngr-donate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New `imbue-mngr-donate` plugin: `mngr donate` spends spare Claude capacity (from the `imbue-mngr-usage` snapshot, a dependency) on a donation skill instead of letting idle quota expire. When the 5h window has budget and the week is under pace, it launches a headless Claude agent that runs the skill to completion, then auto-cleans up; `--start`/`--stop` install/remove a launchd LaunchAgent (macOS) so idle quota drains on a schedule (launchd, not cron, so it runs in the login session where Claude's keychain token lives). The donation skill (code + prompts) is owned by its upstream git repo -- `mngr donate` checks it out at `--skill-ref` (a branch to track, or a pinned commit for a reviewed version) into a host-dir cache and points the agent at it, so the skill updates without an mngr release.
9 changes: 9 additions & 0 deletions libs/mngr_donate/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Project-level conftest for mngr_donate."""

from imbue.imbue_common.conftest_hooks import register_conftest_hooks
from imbue.mngr.utils.logging import suppress_warnings
from imbue.mngr.utils.plugin_testing import register_plugin_test_fixtures

suppress_warnings()
register_conftest_hooks(globals())
register_plugin_test_fixtures(globals())
Empty file.
Loading
Loading