Skip to content

Commit 099fccd

Browse files
mabry1985claude
andauthored
v0.6.0: the GitHub rail works out of the box on a fresh install — status probe, setup card, token secret, classified errors, no-raise tools (#27)
* feat(gh_cli): resolve the gh binary, inject the config token, classify errors - resolve_gh(): shutil.which first, then /opt/homebrew/bin, /usr/local/bin, ~/.local/bin, /usr/bin (a desktop build launched without a shell PATH still finds a brew/apt gh); cached per process, re-resolved on a stale path. - set_token_getter(): the plugin's github.token secret is injected into every gh run as GH_TOKEN/GITHUB_TOKEN, winning over an ambient env token; read live so a token pasted in Settings works without a restart. - check_gh_error() now CLASSIFIES: gh exit 4 / 'gh auth login' stderr → not authenticated (with the fix); GraphQL could-not-resolve / HTTP 404 → repo not found; 403/429 + rate limit → rate limit hit (retry-after when gh says); binary missing → not installed (looked in …). Everything else keeps the 'Error (gh exit N): …' shape existing callers match on. - parse_json()/dicts(): the one place a gh JSON body is type-checked, so a dict-for-a-file / bare scalar can never AttributeError through a tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(repos): live default-repo getter, checkout origin fallback, named default_repo error (#23) - resolve_repo() accepts a zero-arg getter for the default (evaluated only when no explicit repo was passed) so the tools / /issue read the LIVE config instead of a register-time snapshot — an onboard_project or a Settings edit mid-session is seen by the next call. - default_repo_error(): a malformed github.default_repo (not owner/name) is a NAMED error — in /issue, the routes and the views — never fed to gh. Closes #23. - projects.py: last-resort repo source — parse the origin remote of every registry project path (those without a github: binding first) and of project_board.repo (skipping the board's '.' sentinel) into owner/name; HTTPS/SSH/scp shapes, github.com-anchored, cached per path for a minute. Picker order: explicit → registry bindings → remotes, deduped. - The GITHUB_DEFAULT_REPO / GH_REPO env fallback stays (a test depends on it) but is logged at INFO whenever it decides the repo — never silent. - gh_issue.py docstring: '/issue is the user-only chat path; the github_create_issue agent tool exists behind github.write' (the old 'deliberately NOT an agent tool' was false since write_tools shipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tools): never raise — repo_contents on a file, typed JSON everywhere, github_status, docstring truth - github_repo_contents: the contents API returns a dict for a FILE; iterating it raised AttributeError through the tool layer and killed the agent's turn. Now: "Error: '<path>' is a file, not a directory — use github_read_file". - Every gh JSON body goes through parse_json()/dicts() (get_pr/get_issue/ list_issues/ci_runs/review guards) — a list, scalar, null row or garbage is an Error string, not an exception. The sweep found get_pr and list_issues raising on the wrong JSON type too. - New read tool github_status (always on, no write gate): one paragraph — gh installed? authenticated? as whom? which default repo? — so the model self-diagnoses instead of guessing from stderr. - tests/test_no_raise_sweep.py: enumerates EVERY tool register() produces (read+write+review, 23) and invokes each against a run_gh stub returning dict / list / list-of-nulls / empty / garbage / number / string / error / auth / not-found / missing-binary / timeout, asserting a str comes back. Plus a real missing-binary pass through run_gh itself. The arg table must match the registered set, so a new tool can't dodge the sweep. - Docstring truth: github_read_file's description (what the model sees) no longer carries 'TODO(team): implement via …'; the 'STUBBED' module/section comments are gone; factories accept a getter for the default repo. - check_gh_error(..., repo=) everywhere so a 404 names the repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(first-run): /status route, setup card in both views, setup-gap probe, live config wiring - status.py: compute_status() probes gh --version + gh auth status --json hosts (text fallback for an older gh) → {gh_path, gh_version, authenticated, login, host, token_source, error, default_repo, repos}; the ACTIVE account decides; bounded timeout; never raises. summarize_status() is the github_status tool's paragraph; gaps_for() / report_gaps() push 'gh' / 'auth' messages to the host's report_setup_gap seam (guarded — no seam, no-op) and clear them when healthy; probe_in_background() runs that off the register() hot path in a daemon thread, only when the seam exists. - GET /api/plugins/github/status (gated data router) — plus default_repo_error. resolve_config() is the ONE picker/default computation shared by /config, /status and POST /issue: a malformed default_repo is named, kept out of the picker, and the default falls through (#23). - view.py: a shared setup card (_SETUP_CSS/_SETUP_JS spliced into BOTH pages) rendered from /status inside the kit boot only (post-handshake): gh missing → install + sign-in copy; not signed in → gh auth login or paste a token in Settings ▸ GitHub (github.token); malformed default repo → the named error; a Re-check button. The existing 'No repositories configured' state stays (and the new-issue form now says it too). - __init__.py: everything closes over LIVE getters (registry.live_config, else the snapshot) — the tools' default repo, the picker, /issue, the data router, and the github.token secret via set_token_getter; the setup probe starts when the host has the seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: v0.6.0 — token secret in the manifest, ruff pinned, docs match the code, release recipe - protoagent.plugin.yaml: version 0.6.0; secrets: [token] + the Settings field 'GitHub token (PAT) — optional; gh auth login also works'; the auth precedence and repo-resolution comments match gh_cli.py / projects.py. - pyproject.toml: 0.6.0 (test_version.py lockstep). - ruff==0.15.10 pinned in requirements-dev.txt and installed from there in ci.yml (the protoAgent core / projectBoard pin) — a floating ruff fails format --check on correctly-formatted code. - README: the real tool inventory — 11 read / 8 write / 3 review, all live (was 8/3 with five 'stubbed'); auth precedence; repo sources; the setup card. - PROTO.md: §3 file map (status.py, projects.py, review_tools.py), §4 '/issue is the user-only chat path; github_create_issue exists behind github.write' + the live-config rule, §5 inventory + error classification + first-run status + the no-raise sweep, §6 never-raise rule, NEW §7 release recipe (tag -a / push / gh release create; no CHANGELOG — the PR body is it). - CLAUDE.md / AGENTS.md pointers no longer mention 'the stubs'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): never cache a gh miss, report gaps from every status probe, gh-native token precedence, no-raise spawn, off-loop picker Adversarial review of #27, all items: BLOCKERS 1. resolve_gh() caches only a HIT; a miss is re-resolved every call, and compute_status() drops the cache first — so Re-check (and the next tool) sees gh installed after boot, and sees it removed. gh_available() now uses the same resolver (it disagreed with the tools before). Tests cover absent→installed and installed→removed. 2. compute_and_report(): /status AND the github_status tool now report to report_setup_gap (edge-triggered — failing key gets its message, passing key gets None) so the host banner clears on the very request that sees the recovery. build_data_router(cfg, registry=) and get_read_tools(..., registry=) carry the registry; register() passes it. MINORS 3. Token precedence matches gh: only a non-empty config token is injected (GH_TOKEN + GITHUB_TOKEN); otherwise the env passes through untouched so gh's own GH_TOKEN > GITHUB_TOKEN > keyring applies. resolve_token()/ token_source() check GH_TOKEN first. Manifest/README/PROTO wording. 4. run_gh(): trailing 'except OSError' -> (126, '', 'could not run gh at ...') (ENOEXEC/EMFILE), contextlib.suppress(ProcessLookupError) around kill(). Sweep extended: every tool x {ENOEXEC, EMFILE, kill-after-exit}. 5. effective_default_repo() takes the picker as a GETTER and short-circuits when default_repo is set (no git fork on the configured path); the data routes and github_status resolve via asyncio.to_thread; the probe thread primes the remote cache; TTL 60s -> 10min. Tests assert the picker is never computed with a default set, and that routes resolve off-loop. 6. auth_hint(token_source) — a rejected env/config token says 'fix/unset it' / 'replace it in Settings > GitHub', not 'run gh auth login'. Used by the classified error, the summary, the gaps, and the views' card. The gh 401 body is trimmed to its status line. 7. Tool counts: 12 read / 8 write / 3 review = 23 everywhere. NITS - github_path_exists classifies FIRST (error_kind): auth/rate-limit/missing binary are UNVERIFIED errors, never a MISSING verdict; the 404 wording names the inaccessible-repo case. - README/PROTO state #23's named error is on the plugin's GET /config + the card (on read), not on Settings save. - gaps_for caps the why fragment at 80 chars so the host's ~300-char banner keeps the hint. - conftest: an autouse fixture clears GH_TOKEN/GITHUB_TOKEN, the token getter and the gh cache per test, so a dev box's ambient token can't skew hint assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9f7f36e commit 099fccd

27 files changed

Lines changed: 2744 additions & 242 deletions

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ jobs:
2121
python-version: "3.12"
2222

2323
- name: Install dev deps
24-
run: pip install -r requirements-dev.txt ruff
24+
# ruff is PINNED in requirements-dev.txt (0.15.10, the protoAgent/projectBoard pin) —
25+
# a floating ruff fails `format --check` on correctly-formatted code.
26+
run: pip install -r requirements-dev.txt
2527

2628
- name: Lint
2729
run: ruff check . && ruff format --check .

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
**Canonical instructions live in [PROTO.md](./PROTO.md).** Read it before editing — it
44
owns the stack, the lint+test gate, the per-agent gating design (**§4, don't break
5-
it**), what to build (the stubs, §5), and the rules (§6, incl. host-free imports and
6-
plain-string `@tool` docstrings). This file is a thin pointer; edit PROTO.md.
5+
it**), the tool inventory + error classification (§5), the rules (§6, incl. host-free
6+
imports, plain-string `@tool` docstrings, and never-raise tools), and the release
7+
recipe (§7). This file is a thin pointer; edit PROTO.md.

‎CLAUDE.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
**Canonical instructions live in [PROTO.md](./PROTO.md).** Read it before editing — it
44
owns the stack, the lint+test gate, the per-agent gating design (**§4, don't break
5-
it**), what to build (the stubs, §5), and the rules (§6, incl. host-free imports and
6-
plain-string `@tool` docstrings). This file is a thin pointer; edit PROTO.md.
5+
it**), the tool inventory + error classification (§5), the rules (§6, incl. host-free
6+
imports, plain-string `@tool` docstrings, and never-raise tools), and the release
7+
recipe (§7). This file is a thin pointer; edit PROTO.md.

‎PROTO.md‎

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,40 @@ tools over the `gh` CLI, with **per-agent write gating**. The host loads it via
1313
| Layer | What |
1414
|---|---|
1515
| Runtime | Python ≥ 3.11; `langchain-core` (`@tool`) is provided by the host |
16-
| Auth | `gh` ambient auth, or `GITHUB_TOKEN`/`GH_TOKEN` from the env |
16+
| Auth | a non-empty `github.token` secret (Settings) is injected and wins; else the env passes through and gh's own order applies (`GH_TOKEN` > `GITHUB_TOKEN` > `gh auth login` keyring) |
1717
| Repo | `protoLabsAI/github-plugin`, ships `enabled: false` (install ≠ enable ≠ trust) |
1818

1919
## 2. Commands — the PR gate
2020

2121
These must pass before a PR opens (host-free — no protoAgent needed):
2222

2323
```bash
24-
pip install -r requirements-dev.txt ruff
24+
pip install -r requirements-dev.txt # includes the PINNED ruff==0.15.10
2525
ruff check . && ruff format --check . # lint + format
2626
pytest -q # the suite
2727
```
2828

29-
There is no other runner. `ruff` + `pytest` are the sole gate.
29+
There is no other runner. `ruff` + `pytest` are the sole gate. `ruff` is pinned (the
30+
same `0.15.10` as protoAgent core and projectBoard-plugin) in BOTH `requirements-dev.txt`
31+
and `ci.yml` — a floating ruff fails `format --check` on correctly-formatted code; bump
32+
the pin in both places together.
3033

3134
## 3. Where everything lives
3235

3336
```
34-
protoagent.plugin.yaml # manifest (id: github, config_section: github; write/default_repo/repos)
35-
__init__.py # register() — gating wiring (read always; write iff github.write) + /issue
36-
gh_cli.py # vendored async `gh` runner (run_gh, check_gh_error, bad_repo)
37-
read_tools.py # 8 read tools (6 ported core + read_file/repo_contents)
37+
protoagent.plugin.yaml # manifest (id: github, config_section: github; write/default_repo/repos + the `token` secret)
38+
__init__.py # register() — gating wiring (read always; write iff github.write) + /issue + live-config getters
39+
gh_cli.py # vendored async `gh` runner: binary resolution (PATH + install dirs), token
40+
# injection (config secret > env), check_gh_error CLASSIFICATION, bad_repo
41+
status.py # the first-run probe: compute_status / summarize_status / report_gaps (setup-gap seam)
42+
projects.py # repo sources: host projects: registry (ADR 0095) + checkout `origin` remote parsing
43+
read_tools.py # 12 read tools (6 ported core + file/contents/pr-file/path-exists/pr-diff + github_status)
3844
write_tools.py # 8 write tools (create/edit/merge/close/comment/labels/assignees) — gated
39-
gh_issue.py # /issue chat command logic (user-only; gate-checked; configured repo)
40-
api.py # routers — public PAGES (view/new-issue) + gated data routes (config/issues/prs/issue)
41-
view.py # PAGE = read-only board (Issues/PRs tabs + repo picker); NEW_ISSUE_PAGE = file-an-issue form. --pl-* themed
42-
tests/ # host-free pytest (gating + version coherence + routes via TestClient)
45+
review_tools.py # 3 verdict tools (comment/approve/request-changes, guarded) — gated
46+
gh_issue.py # /issue chat command logic + repo resolution (resolve_repo, default_repo_error)
47+
api.py # routers — public PAGES (view/new-issue) + gated data routes (config/status/issues/prs/issue)
48+
view.py # PAGE = read-only board; NEW_ISSUE_PAGE = file-an-issue form; shared setup card. --pl-* themed
49+
tests/ # host-free pytest (gating + version coherence + routes via TestClient + the no-raise sweep)
4350
```
4451

4552
**Console surfaces (ADR 0026/0038/0042/0057).** `register()` mounts two routers when the
@@ -63,22 +70,39 @@ Filing lives in the widget + palette, NOT the board (keep the board read-only).
6370
agent stays read-only; a coding/PM agent gets write. `tests/test_register.py` asserts
6471
both halves — keep it green.
6572

66-
**`/issue` is a user-only chat command, not an agent tool** — creating an issue is a
67-
write the model must not do autonomously, so `register()` registers it via the host's
68-
`register_chat_command` seam (the logic lives in `gh_issue.py`). The call is guarded
69-
by `hasattr(registry, "register_chat_command")`, so on an older host without the seam
70-
`/issue` is skipped and the tools still load (degrade-safe). It routes to the
71-
configured `default_repo`/`repos` (never a silent default). `tests/test_issue_command.py`
72-
asserts both the seam-present and legacy-host paths — keep them green.
73-
74-
## 5. Tools (all implemented)
73+
**`/issue` is the user-only chat path; the `github_create_issue` agent tool exists
74+
behind `github.write`.** A PERSON files from the composer on any agent — including a
75+
read-only one — via `/issue`, registered through the host's `register_chat_command`
76+
seam (the logic lives in `gh_issue.py`); the MODEL files via `github_create_issue`
77+
only when the agent's write gate is on (the PM archetype requires it). Both go
78+
through the same `file_issue` (gate check + `gh issue create`), so they can't
79+
diverge. The seam call is guarded by `hasattr(registry, "register_chat_command")`, so
80+
on an older host without it `/issue` is skipped and the tools still load
81+
(degrade-safe). It routes to the configured `default_repo`/`repos` (never a silent
82+
default). `tests/test_issue_command.py` asserts both the seam-present and
83+
legacy-host paths — keep them green.
84+
85+
**Config is read LIVE.** `register()` builds getters over `registry.live_config` (the
86+
snapshot on an older host) and every consumer — the tools' default repo, `/issue`, the
87+
data routes, the `token` secret — reads through them per call. Never capture a config
88+
value at register time: an `onboard_project` mid-session, or a token pasted in
89+
Settings, must be seen by the very next call.
90+
91+
## 5. Tools (all implemented — 12 read / 8 write / 3 review = 23)
7592

7693
Each tool mocks `run_gh` in its test and asserts the exact argv + readable errors.
94+
`tests/test_no_raise_sweep.py` additionally invokes EVERY registered tool against a
95+
`run_gh` stub returning a dict / a list / garbage / an error and asserts a `str` comes
96+
back — a tool that raises kills the whole agent turn, so no tool may. Add a tool ⇒ the
97+
sweep covers it automatically (it enumerates `register()`'s output).
7798

7899
**Read (always on)** — 6 ported core tools (`github_get_pr`, `github_get_issue`,
79-
`github_list_issues`, `github_get_commit_diff`, `github_ci_runs`, `github_run_failure`)
80-
plus `github_read_file` (`gh api .../contents/{path}`, raw) and `github_repo_contents`
81-
(directory listing).
100+
`github_list_issues`, `github_get_commit_diff`, `github_ci_runs`, `github_run_failure`),
101+
`github_pr_diff`, the content readers (`github_read_file` — raw file; `github_read_pr_file`
102+
— a file at a PR's head; `github_repo_contents` — directory listing, says "is a file —
103+
use github_read_file" on a file; `github_path_exists` — the EXISTS/MISSING probe), and
104+
`github_status` — is `gh` installed / authenticated / as whom / which default repo, the
105+
self-diagnosis tool the model calls when another tool errors (no `write` gate).
82106

83107
**Write (gated on `github.write`)** —
84108
`github_create_issue` / `github_comment` / `github_create_pr` (return the new URL),
@@ -87,23 +111,81 @@ plus `github_read_file` (`gh api .../contents/{path}`, raw) and `github_repo_con
87111
`github_close` (close/reopen issue|pr), and `github_set_labels` / `github_set_assignees`
88112
(`gh {issue,pr} edit --add/--remove-{label,assignee}`). Issue-vs-PR ops take `kind`.
89113

114+
**Review (gated on `github.write`)** — `github_review_comment` (always allowed),
115+
`github_review_approve` / `github_review_request_changes` (refused while CI is pending
116+
or unreadable, and on the agent's own PR — guards live in the tool, below the model).
117+
90118
New write op? Validate `bad_repo()`, build argv, `run_gh()`, degrade to `Error: ...`,
91119
add it to `get_write_tools()`'s return list **and** `WRITE_TOOLS` in `test_register.py`,
92120
and mirror an existing test. Anything irreversible (merge) must be `confirm`-guarded.
93121

122+
**Errors are classified** (`gh_cli.check_gh_error`, categories from `error_kind`): `gh`
123+
exit 4 / "gh auth login" ⇒ `Error: GitHub CLI is not authenticated — …` with the hint
124+
branched on `token_source()` (a rejected env/config token says "replace/unset it", not
125+
"run gh auth login"); GraphQL "Could not resolve to a Repository"
126+
⇒ `Error: repo 'o/n' not found or not accessible`; HTTP 404 ⇒ not found (with the repo
127+
named when known); 403/429 + "rate limit" ⇒ `Error: GitHub API rate limit hit — retry …`;
128+
binary missing ⇒ `Error: gh CLI is not installed or not on PATH (looked in …)`. Anything
129+
else keeps the `Error (gh exit N): <stderr>` shape. Add a category here, not in a tool.
130+
131+
**First-run status** (`status.py`): `GET /api/plugins/github/status` → `{gh_path,
132+
gh_version, authenticated, login, host, token_source, error, default_repo, repos,
133+
default_repo_error}` from `gh --version` + `gh auth status --json hosts` (text fallback
134+
for an older `gh`); never raises. The views render a setup card from it; the
135+
`github_status` tool returns `summarize_status()`. **Every status computation reports**
136+
to the host's `report_setup_gap` seam (guarded — no seam, no-op): `probe_in_background()`
137+
at register time (a daemon thread, off the boot path) and `compute_and_report()` from
138+
`/status` and the tool, edge-triggered (a failing key gets its message, a passing key
139+
gets `None`) so the operator banner clears on the very Re-check that sees the recovery.
140+
Each probe drops the cached `gh` path first — a miss is never cached — so a `gh`
141+
installed after boot is found by the next call.
142+
**Never block the loop on the picker**: `effective_default_repo` takes the picker as a
143+
getter and short-circuits when `default_repo` is set; the routes and `github_status`
144+
run the resolution via `asyncio.to_thread`; checkout remotes are cached 10 min.
145+
94146
## 6. Rules
95147

96148
- **Host-free.** NEVER import `graph.*` / `plugins.*` at module top — the suite runs
97149
with only `requirements-dev.txt`. Keep any host imports lazy (inside functions).
98150
- **`@tool` docstrings must be PLAIN string literals** — an f-string docstring makes
99151
`__doc__` None and the tool ships with no description (the model can't see it).
100-
- **Every tool requires an explicit `owner/name` repo** — validate with `bad_repo()`;
101-
there is no silent default (a forgotten repo must error, not hit the wrong repo).
152+
- **Every tool requires an `owner/name` repo** — validate with `bad_repo()`. The
153+
fallback when the arg is omitted is the LIVE configured default (`default_repo` >
154+
first picker entry; the picker = `github.repos` ∪ the host's `projects:` registry ∪
155+
the `origin` remote of each registered checkout / `project_board.repo`), then the
156+
`GITHUB_DEFAULT_REPO` / `GH_REPO` env **logged at INFO when it fires**. No repo
157+
anywhere ⇒ an error, never a guess. A malformed `default_repo` is the NAMED
158+
`default_repo_error` (#23) on the plugin's `GET /config` / `/status`, the issue route,
159+
the `github_status` tool and the views' setup card — i.e. on READ; the host's Settings
160+
save itself is not gated (the plugin has no save route).
161+
- **Never raise out of a tool.** A tool that raises kills the agent's whole turn. Every
162+
`gh` result is typed-checked before use (the contents API returns a dict for a file,
163+
a list for a directory); the no-raise sweep test enforces this for every tool.
102164
- **DO NOT FABRICATE.** Use real `gh` invocations; verify the actual `gh api` shape
103165
before relying on it. No placeholder/guessed command flags.
104166
- **Don't add runtime pip deps.** Test-only deps go in `requirements-dev.txt`; real
105167
runtime deps would go in the manifest's `requires_pip` (operator-installed).
106168

107-
## 7. Agent-scratch
169+
## 7. Release
170+
171+
A release is a version bump + a tag + a GitHub release; hosts pin the tag in
172+
`plugins.lock`. `tests/test_version.py` asserts the manifest and `pyproject.toml`
173+
versions match — bump BOTH.
174+
175+
```bash
176+
# 1. bump `version:` in protoagent.plugin.yaml AND `version =` in pyproject.toml (lockstep)
177+
# 2. land the PR on main (the gates above must be green)
178+
# 3. tag + release from the merged main
179+
git checkout main && git pull
180+
git tag -a vX.Y.Z -m "vX.Y.Z — <one-line summary>"
181+
git push origin vX.Y.Z
182+
gh release create vX.Y.Z --title "vX.Y.Z" --notes "<the PR body / changelog>"
183+
```
184+
185+
There is no CHANGELOG file — the PR body is the changelog; paste it into the release
186+
notes. Installed hosts pick the new version up with
187+
`python -m server plugin update github` (or by re-pinning `plugins.lock`).
188+
189+
## 8. Agent-scratch
108190

109191
`.proto/` is the coding agent's own scratch — gitignored, never commit.

‎README.md‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,29 @@ config sets `github.write: true`** — so a research/Lead agent stays read-only
88
coding/PM agent gets write, purely by its own per-instance config. Supersedes the
99
read-only in-tree `github` plugin.
1010

11-
## Tools
11+
## Tools (all implemented)
1212

13-
**Read** (always): `github_get_pr`, `github_get_issue`, `github_list_issues`,
14-
`github_get_commit_diff`, `github_ci_runs`, `github_run_failure`, `github_read_file`*,
15-
`github_repo_contents`*.
13+
**Read** (always, 12): `github_get_pr`, `github_get_issue`, `github_list_issues`,
14+
`github_get_commit_diff`, `github_pr_diff`, `github_ci_runs`, `github_run_failure`,
15+
`github_read_file`, `github_read_pr_file`, `github_repo_contents`, `github_path_exists`,
16+
and `github_status` (is `gh` installed / signed in, as whom, which default repo — the
17+
self-diagnosis probe the model calls when another tool errors).
1618

17-
**Write** (only when `github.write: true`): `github_create_issue`*, `github_comment`*,
18-
`github_create_pr`*.
19+
**Write** (only when `github.write: true`, 8): `github_create_issue`, `github_comment`,
20+
`github_create_pr`, `github_edit_pr`, `github_merge_pr` (`confirm`-guarded),
21+
`github_close`, `github_set_labels`, `github_set_assignees`.
1922

20-
*\* stubbed — being built out; see [PROTO.md §5](./PROTO.md).*
23+
**Review** (also behind `github.write`, 3): `github_review_comment`,
24+
`github_review_approve`, `github_review_request_changes` — the formal verdict tools,
25+
with the CI-terminal and self-review guards enforced inside the tool (ADR 0078).
26+
27+
Plus the **user-only `/issue` chat command** (file an issue from the composer on any
28+
agent, without the model) and two console views — the read-only Issues/PRs **board**
29+
and the **New issue** form — both of which show a **setup card** when `gh` is missing
30+
or not signed in.
31+
32+
Every tool returns a readable, classified error instead of raw `gh` stderr: not
33+
authenticated (with the fix), repo not found, API rate limit, `gh` not installed.
2134

2235
## Install & enable
2336

@@ -31,15 +44,31 @@ Install ≠ enable. To turn it on, add to `config/langgraph-config.yaml`:
3144
plugins:
3245
enabled: [github]
3346
github:
34-
write: false # read-only; set true ONLY for agents that should mutate GitHub
47+
write: false # read-only; set true ONLY for agents that should mutate GitHub
48+
default_repo: "" # owner/name — the repo tools and /issue use when none is passed
49+
repos: [] # repo picker list; the host's projects: registry is added automatically
3550
```
3651
37-
Auth: `gh` uses its own ambient auth (`gh auth login`) or `GITHUB_TOKEN`/`GH_TOKEN`.
52+
**Auth**: a non-empty `github.token` secret (Settings ▸ GitHub — a personal access token
53+
with `repo` scope) is injected into every `gh` run and wins. Otherwise the environment is
54+
passed through untouched and `gh`'s own precedence applies: `GH_TOKEN` > `GITHUB_TOKEN` >
55+
the `gh auth login` keyring. Public-repo reads need none at low volume. When auth fails,
56+
the error says which of those was rejected and what to do about it.
57+
`gh` is found on PATH or in the usual install dirs (`/opt/homebrew/bin`, `/usr/local/bin`,
58+
`~/.local/bin`, `/usr/bin`) — a desktop build launched without a shell PATH still works.
59+
60+
**Repos**: a tool's omitted `repo` falls back to `default_repo`, else the first picker
61+
entry. The picker is `github.repos` ∪ the host's managed-projects registry (ADR 0095,
62+
`projects:` entries with a `github:` binding) ∪ — last resort — the `owner/name` parsed
63+
from the `origin` remote of each registered checkout (and `project_board.repo`). All of
64+
it is read live: a Settings edit or an `onboard_project` mid-session is seen by the
65+
next call. A malformed `default_repo` is a named error on the plugin's `GET /config` and
66+
in the views' setup card (not on Settings save), never fed to `gh`.
3867

3968
## Develop
4069

4170
```bash
42-
pip install -r requirements-dev.txt ruff
71+
pip install -r requirements-dev.txt # pins ruff==0.15.10
4372
ruff check . && ruff format --check . && pytest -q
4473
```
4574

0 commit comments

Comments
 (0)