Back to Hermes Series

GitHub Issues Became the Queue

Post 23 - GitHub Issues Became the Queue
Captain's Log

Loose markdown was good for local notes and bad as a shared queue. Shared agent work moved into the GitHub Issues for the repo that owns the work. A ProtonPass-backed GitHub App token path gives repo-scoped access. Labels route work. Comments hold the run log. Close means the acceptance check passed.

per repoissue queue
0personal tokens required
4installed permission scopes

GitHub issues became the queue

Agent markdown had started to multiply.

One agent left handoff.md. Another left notes.md. A third wrote next_steps.md. Some files were useful. Some were stale by the next run. All of them had the same defect.

They looked shared because they lived in a repo or workspace. In practice, they were loose files with no owner, no state, no label, and no comment trail. The next worker had to infer too much.

The control surface was already in the stack.

GitHub Issues.

An issue is small and durable. It has a title, a body, labels, comments, state, links, timestamps, and an API. It lives beside the code. Humans already know how to read it. Agents can read it without inventing a new protocol.

Each repo has its own issue queue. Work stays beside the project it belongs to. The same issue format and labels repeat across repos.

The setup needs a safe identity.

Why a GitHub App

A personal access token is too wide for this job. It belongs to a person. It tends to collect extra scope. It also makes every automated action look like the person did it.

A GitHub App is narrower.

The App gets installed on selected repos. It receives the permissions the workflow needs. It mints short lived installation tokens. The agent uses those tokens to list issues, create issues, comment, label, create branches, and open pull requests.

The installed scopes are explicit:

textMetadata: read
Contents: read and write
Issues: read and write
Pull requests: read and write

Those scopes are still repo-scoped. The App can only act where it is installed.

The boundary is simple:

  • the App is the actor
  • repo installation controls where it can work
  • App permissions control what it can do
  • labels control how agents route work
  • comments hold the work log
  • close means the task passed its check

That gives each project a queue with permissions, history, and a clean API.

The setup

Create a GitHub App in developer settings.

Use a fictional example name in public docs. Agent Reporter is enough for the walkthrough.

Install the App only on the repos where agents should work. Selected repos are better than all repos.

The private key does not sit on disk as a .pem file. It lives in ProtonPass. A runtime wrapper resolves a pass:// reference and injects the key into the process environment only for the lifetime of the command.

The checked-in template has references, not secrets.

bash# .env.tpl
GITHUB_APP_ID=123456
GITHUB_APP_INSTALLATION_ID=12345678
GITHUB_APP_PRIVATE_KEY=pass://vault-ref/item-ref/note

At runtime, pass-cli resolves the secret and launches the command.

bashpass-cli run --env-file .env.tpl -- bash -c '
  export GH_TOKEN=$(github-agent-token)
  gh issue create \
    --repo OWNER/REPO \
    --title "[Agent Task] Fix import validation" \
    --body-file issue.md \
    --label agent-task \
    --label status:ready
'

The key material enters the environment. It is not written to .env. It is not written to a private key file. When the command exits, the plaintext secret is gone with the process.

The token flow is mechanical:

  1. Read the App id, installation id, and private key from the injected environment.
  2. Sign a JWT with the App private key.
  3. Request an installation token from GitHub.
  4. Export the token as GH_TOKEN for the gh CLI.
  5. Refresh the token when it expires.

The installation id is a config value. It is assigned to the App installation for the org or account. Normal runs use that value directly.

The token endpoint is direct.

textPOST /app/installations/{installation_id}/access_tokens

The gh CLI edge

Installation tokens work, but the GitHub CLI has a rough edge.

gh auth status can report an installation token as invalid even when the same token works for real API calls. The useful test is the operation itself, not the status banner.

Export the token as GH_TOKEN. Do not rely on GITHUB_TOKEN for this path.

If a high-level command fails with a GraphQL auth error, drop to REST.

bashgh api \
  --method POST \
  repos/OWNER/REPO/issues \
  -f title='[Agent Task] Fix import validation' \
  -f [email protected]

The REST path is reliable.

The issue shape

A weak issue says:

textInvestigate import bug.

That is too thin.

A useful issue gives the next agent enough context to act.

textImport fails when UPC-A values lose the leading zero.

Check:
- src/import/normalize_gtin.py
- tests/test_gtin_normalization.py

Run:
- pytest tests/test_gtin_normalization.py

Accept when:
- UPC-A keeps the leading zero
- EAN-13 stays 13 digits
- GTIN-14 stays 14 digits
- the import report shows no format loss

The issue body is an operating packet. It should name the file paths, commands, expected result, source artifacts, and blockers. If the next agent has to guess the missing half of the task, the issue failed.

Labels and comments

Labels are routing.

textagent-task
source:fresh
status:ready
status:blocked
needs-human
data-quality
bug

Use labels that survive search and filtering.

Comments are the work log. An agent comments when it starts. It comments with findings. It links branches, pull requests, logs, and artifacts. It does not close the issue because it got tired. It closes the issue when the acceptance check passes.

Agent systems often fail by producing clean summaries over unfinished work. Issues reduce that failure mode because the task state is outside the agent's context window. The next worker can read the record. A human can inspect the trail. Each repo keeps its own record.

The new surface

Work now starts with the repo that owns the task.

bashgh issue list --repo OWNER/REPO --label agent-task --label status:ready
gh issue view 42 --repo OWNER/REPO --comments

A human or agent chooses the next item from that project queue, reads the operating packet, does the work, and writes the run log back to the issue.

A background service can come later if volume requires it. The first useful version uses the GitHub list directly. GitHub already stores the state. The convention makes the state inspectable before any worker starts.

Markdown still has a job. I use it for notes, drafts, plans, and local memory inside a run.

Shared work moved to the issue queue for the repo that owns the work.

The pattern repeats per project: same packet shape, same routing labels, same acceptance rule. Each repo keeps its own work record beside the code it changes.

The repo issue queue becomes the work surface. The GitHub App supplies the scoped credential.

Shared agent work surface
Agent outputnotes · findings · blockers
GitHub Appselected repos · scoped token
GitHub Issuetask packet · state · history
Labelsroute the work
Commentsrecord the run
Close eventacceptance check passed
needs-humanapproval, missing source, or unsafe action
Project queueeach repo keeps its own record
← Previous postNext post →