You are working on an Agent Zero plugin that gives the agent real GitHub powers through the gh CLI,
authenticated from the user's GitHub token. A mistake here writes that token to disk where it can leak
into a shipped artifact or a backup, installs a tampered binary, opens PRs against the wrong repo, or
wedges the user's boot / save / uninstall. Follow these rules exactly. They are not suggestions.
A self-contained A0 plugin (id github): it installs the gh CLI into its own folder, authenticates
it statelessly from an A0 Secret, and ships a pack of process skills (github-open-pr,
github-triage-issue, github-review-pr, github-search, github-watch, github-create-release)
whose tested gh command templates teach the agent the workflow, not just the tool. It also
self-registers an optional hourly github-watch scheduled task. Publishable (MIT), model-agnostic,
uninstall-clean. The surface: a gh/git auth wrapper + the skills + a few extensions + one config UI.
- Auth is STATELESS — the token is NEVER persisted. It lives only in A0 Secrets (
usr/secrets.env, key fromsecret_key, defaultGITHUB_TOKEN) and is read fresh per operation by the/usr/local/bin/ghwrapper (gh_setup.ensure_wrapper), which exports it asGH_TOKENfor that oneghprocess. NEVER write it to aghlogin file, a git credential store, an env file, or a log.gittransport uses the SAME wrapper via the singlecredential.https://github.1485827954.workers.dev.helperline — never a stored credential. - Verify the binary before you trust it.
gh_setup.ensure_binarydownloadsghand MUST pass_verify_checksumagainst GitHub's publishedgh_<ver>_checksums.txtSHA-256 before install. A checksum mismatch or missing entry REFUSES the install — never relax or skip this. - Touch nothing outside the plugin folder except the two things you remove on uninstall. The only
external state this plugin creates is the
/usr/local/bin/ghwrapper and that onegithub.1485827954.workers.devgit credential-helper line.gh_setup.cleanup()(called byuninstall()inhooks.py) must remove EXACTLY those;watch_schedule.remove()removes the scheduled task. Add a new external side effect → add its reversal in the same change. Runtime state stays underusr/github-watch/. - Every entry point is best-effort — never block boot, save, or uninstall. All of
gh_setupandwatch_schedulelog-and-return on error, never raise. Thestartup_migrationextensions, thesave_plugin_confighook, and the uninstall path each tolerate a failure of any other. A setup failure must degrade to "GitHub unavailable," never a crashed container. - Self-registered cron must be VALID — never emit
*/24.watch_schedule._cronmaps an interval token / custom cron to aTaskSchedule; an N≥24h cadence becomes a fixed daily run (minute=0 hour=0) because*/24is an invalid hour step (max 23) that crash-loops the scheduler. A customwatch_cronis gated by_cron_ok(must be a 5-field crontab) before use. - The watch context reset runs at
monologue_START, not end. Scheduler-driven runs never setcontext.task, so A0 never firesmonologue_end/message_loop_endfor them — onlymonologue_startfires._80_github_resetrebuilds history to ONLY the current run's message (gated on the watch context id +watch_reset_context), bounding the reused task context so it can't grow until it exceeds the model window. Don't move this logic to an end hook — it would never fire. - Delivery tools must fire BEFORE
response. Callingresponseends the task (break_loop) instantly, so any agent-owned delivery (notify_user, telegram_send) must run first. Thewatch_schedule._promptbuilder emits a STRICT-ORDER block ONLY when the agent owns a delivery call; direct-mode Telegram (sent bycheck.py) and the extension digest must instead tell the agent NOT to send, or it double-delivers. Keep these mutually exclusive. - The watch runs check.py exactly once per cycle.
tool_execute_before/_30_github_watch_onceblocks a weak utility model from re-runningcheck.pywithin one scheduled run (RepairableException; keyed on the run'slast_user_message.idso it self-resets each cycle; scoped to the watch context so a manual run is unaffected).helpers/run_once.pyholds the sys-attached state. The task is named "GitHub Watch" (renamed from the pre-1.5.6 "github-watch-poll", migrated in place on reconcile).
- Stdlib-only helpers, A0 reached lazily.
helpers/gh_setup.py+helpers/watch_schedule.pyimport A0 (from helpers ...,from usr.plugins.github ...) only INSIDE functions, behind try/except, so a partial framework never breaks import.skills/github-watch/check.pyis stdlib-only (+ optionalgh). - Per change:
py_compileevery.pyvia/opt/venv-a0/bin/python -m py_compile; keep skill command templates tested anddefault_config.yaml↔webui/config.htmlkeys in sync. Bumpplugin.yamlversionon a release, and cut a tagged GitHub Release with user-facing notes. - Keep THIS file current. Update this AGENTS.md in the SAME change whenever you alter a HARD INVARIANT, a cited path/seam/A0 mechanic, or what this plugin is — a stale contract MISLEADS (worse than none). Routine fixes/features that don't change the contract don't touch it.
- Validate in a THROWAWAY, never the live instance. Snapshot/commit the A0 instance into an isolated container and verify there (auth wrapper writes, checksum path, schedule reconcile, config render in a real browser). The maintainer installs the built artifact via the UX — don't live-install.
- Opsec (public repo): no secrets, tokens, IPs, internal hostnames, personal email, or local paths in
shipped files.
config.json,.toggle-*,bin/,dist/,__pycache__/are gitignored (andCLAUDE.md/.claude/if ever added — dev-only). Commits: single human author, GitHub no-reply email, NO AI /Co-Authored-Bytrailers.
- Structure, setup, auth model, config keys, uninstall:
README.md. - Config defaults + inline rationale:
default_config.yaml(the canonical key list + meanings). - Process / workflow: the per-ability
skills/*/SKILL.md(the testedghcommand templates — the agent's source of truth for how to do each GitHub task;github-watch/check.pyis the poll script). - Authoring conventions (this whole plugin framework): the
a0-plugin-templateAUTHORING.md. (This repo has noARCHITECTURE.mdor GitNexus index — README + skills + config are the SoT.)
Verified A0 mechanics (don't re-derive — confirm against the LIVE instance; versions move constantly)
- Hooks:
startup_migration/_50(gh_setup.ensure()each boot — reinstalls wrapper) ·_60(watch_schedule.ensure()reconciles the task) ·monologue_start/_80(watch context reset) ·tool_execute_after/_60(enrich digest, consumespending_digest.jsononce) · theget_chat_model/end/_50reroute (utility-tier swap for agent-mode enrich) ·hooks.pyinstall/uninstall/save_plugin_config. plugins.get_plugin_config(name)returns the savedconfig.jsonOR defaults — NEVER merged._config()falls back todefault_config.yaml, so a fresh install still works; but to add a key to a live install you must save the COMPLETE dict or the others drop.- Scheduler:
scheduler.add_task()is the public path that BOTH appends the task AND creates its context (required — the Tasks list classifies contexts by id viastate_snapshot). The reused task context's agent grows history every run unless reset (see invariant 6).save_plugin_configruns BEFORE the new config is written, so reconcile from the incomingsettings.check_schedule()is a STATELESS ~60s cron window (nolast_runcatch-up — setting it is a no-op for cron tasks;job_loopticks 60s);find_task_by_nameis a SUBSTRING match (probe full names). - Secrets: read at call time from
usr/secrets.env(KEY=value, value may be quoted) by the wrapper, bycheck.py(direct Telegram), and byghnatively viaGH_TOKEN— never from a stored login.