Recover the full gh-backed GitHub issue client abandoned on ward-salvage/ward-b4d43076 (the work #489 shipped only partially) #520

Closed
opened 2026-07-02 07:38:07 +00:00 by coilysiren · 2 comments
Owner

Why

ward#489 ("GitHub as a first-class forge") closed by shipping only the ref/credential half - its own outcome comment says "this lands the ref/credential side, not a full GitHub API client." The fuller implementation an earlier claude engineer actually wrote was salvaged and abandoned when the smaller version landed. It survives, intact, on a reaper salvage branch. Kai asked to recover it.

Source material

Salvage branch: ward-salvage/ward-b4d43076 (tip d100540, one commit, ~842 insertions).

git fetch https://forgejo.coilysiren.me/coilyco-flight-deck/ward.git ward-salvage/ward-b4d43076
git checkout -b recover-b4d43076 FETCH_HEAD   # inspect; do NOT land this branch as-is

Do not merge the branch wholesale - it is based on a now-stale main. Cherry-pick the content onto a fresh branch off current main, reconciling with what #489 already landed (below).

What is worth recovering (new files, absent on main, low conflict)

  • cmd/ward/forge.go (143 lines) - a forge enum (forgeForgejo/forgeGitHub), the issueForge interface abstracting the issue-thread surface across both forges, parseGitHubIssueRef (github.com URL + github.com/owner/repo#N regex), resolveGitHubToken (env-only, no SSM - GITHUB_TOKEN/GH_TOKEN/WARD_GITHUB_TOKEN), and hostForgeClient picking the client per forge.
  • cmd/ward/github_ops.go (217 lines) - githubClient, a gh-backed implementation of every issueForge verb (getIssue, listIssueComments, createIssue, commentIssue, closeIssue, reopenIssue, findOpenIssueByTitlePrefix), bodies passed via --body-file (off argv), state lowercased to match Forgejo.
  • cmd/ward/forge_test.go, cmd/ward/github_ops_test.go - their tests.
  • docs/agent-github.md - the GitHub-forge walkthrough.

The reconciliation (this is the real work)

Main's #489 landing (671c21d) chose a different, smaller model than the salvage branch. You must merge them, not stack them:

  • Duplicate symbol: const githubBaseURL is defined on main (in agent.go) AND on the branch (in forge.go). Keep one.
  • Ref model divergence: main added a Base string field to agentIssueRef and loops issueref.Parse over [forgejoBaseURL, githubBaseURL]. The branch instead added a Forge forge field and a bespoke parseGitHubIssueRef. Pick one coherent model. The forge enum is the richer choice - it already drives baseURL(), gitPushUser() (x-access-token for GitHub), and client selection - so prefer migrating main's Base usage onto forge, but verify nothing else on main reads .Base.
  • agent.go / agent_test.go conflict on both sides - reconcile by hand.

Scope boundary - do NOT recover the reaper routing yet

The branch also edits cmd/ward/container_reap.go (to route the reaper's issue client through the forge abstraction so it can comment on GitHub issues). ward#518 is in flight on that exact file right now. Do not fight it. Recover the forge abstraction + gh client + ref parsing + container wiring (container.go, container_bootstrap.go, entrypoint.sh, containerassets/ - so gh is on PATH and WARD_FORGE/token flow into the container). Leave container_reap.go alone; if the reaper-side GitHub routing is worth having, file a follow-up that layers on top of ward#518 once it lands. Integrate main before your final push regardless.

Done condition

  • warded <github-owner>/<repo>#<n> drives the full loop against a GitHub-hosted issue - clone/push via the env GitHub token, preflight/outcome comments on the GitHub issue - through the recovered gh-backed issueForge client, not just ref parsing.
  • ward exec build/test/vet green; the recovered tests pass.
  • No duplicate githubBaseURL, one ref model, docs/agent-github.md reconciled with the github-token.md/forgejo-git-auth.md docs #489 already shipped.
  • Prefer docs/agent-github.md for docs. Do not touch docs/FEATURES.md front matter (pinned at its char cap, one-agent-at-a-time tonight).
  • ward#489 (the partial landing this completes).
  • ward#518 (in flight on container_reap.go - stay off that file).
  • ward#504 (the salvage issue ward-b4d43076 was filed under).
  • ward#441 / ward#453 (GitHub token is env-only, never SSM - the recovered resolveGitHubToken already honours this).

Filed by Claude (she/her) during the ward#492 burndown, recovering abandoned work at Kai's request.

## Why ward#489 ("GitHub as a first-class forge") closed by shipping only the ref/credential half - its own outcome comment says "this lands the ref/credential side, not a full GitHub API client." The fuller implementation an earlier claude engineer actually wrote was **salvaged and abandoned** when the smaller version landed. It survives, intact, on a reaper salvage branch. Kai asked to recover it. ## Source material **Salvage branch:** `ward-salvage/ward-b4d43076` (tip `d100540`, one commit, ~842 insertions). ```bash git fetch https://forgejo.coilysiren.me/coilyco-flight-deck/ward.git ward-salvage/ward-b4d43076 git checkout -b recover-b4d43076 FETCH_HEAD # inspect; do NOT land this branch as-is ``` Do not merge the branch wholesale - it is based on a now-stale main. Cherry-pick the **content** onto a fresh branch off current `main`, reconciling with what #489 already landed (below). ### What is worth recovering (new files, absent on main, low conflict) - `cmd/ward/forge.go` (143 lines) - a `forge` enum (`forgeForgejo`/`forgeGitHub`), the `issueForge` interface abstracting the issue-thread surface across both forges, `parseGitHubIssueRef` (github.com URL + `github.com/owner/repo#N` regex), `resolveGitHubToken` (env-only, no SSM - `GITHUB_TOKEN`/`GH_TOKEN`/`WARD_GITHUB_TOKEN`), and `hostForgeClient` picking the client per forge. - `cmd/ward/github_ops.go` (217 lines) - `githubClient`, a `gh`-backed implementation of every `issueForge` verb (getIssue, listIssueComments, createIssue, commentIssue, closeIssue, reopenIssue, findOpenIssueByTitlePrefix), bodies passed via `--body-file` (off argv), state lowercased to match Forgejo. - `cmd/ward/forge_test.go`, `cmd/ward/github_ops_test.go` - their tests. - `docs/agent-github.md` - the GitHub-forge walkthrough. ### The reconciliation (this is the real work) Main's #489 landing (`671c21d`) chose a **different, smaller model** than the salvage branch. You must merge them, not stack them: - **Duplicate symbol:** `const githubBaseURL` is defined on main (in `agent.go`) AND on the branch (in `forge.go`). Keep one. - **Ref model divergence:** main added a `Base string` field to `agentIssueRef` and loops `issueref.Parse` over `[forgejoBaseURL, githubBaseURL]`. The branch instead added a `Forge forge` field and a bespoke `parseGitHubIssueRef`. Pick one coherent model. The `forge` enum is the richer choice - it already drives `baseURL()`, `gitPushUser()` (`x-access-token` for GitHub), and client selection - so prefer migrating main's `Base` usage onto `forge`, but verify nothing else on main reads `.Base`. - **agent.go / agent_test.go** conflict on both sides - reconcile by hand. ### Scope boundary - do NOT recover the reaper routing yet The branch also edits `cmd/ward/container_reap.go` (to route the reaper's issue client through the forge abstraction so it can comment on GitHub issues). **ward#518 is in flight on that exact file right now.** Do not fight it. Recover the forge abstraction + `gh` client + ref parsing + container wiring (`container.go`, `container_bootstrap.go`, `entrypoint.sh`, `containerassets/` - so `gh` is on PATH and `WARD_FORGE`/token flow into the container). Leave `container_reap.go` alone; if the reaper-side GitHub routing is worth having, file a follow-up that layers on top of ward#518 once it lands. Integrate `main` before your final push regardless. ## Done condition - `warded <github-owner>/<repo>#<n>` drives the full loop against a GitHub-hosted issue - clone/push via the env GitHub token, preflight/outcome comments on the GitHub issue - through the recovered `gh`-backed `issueForge` client, not just ref parsing. - `ward exec build/test/vet` green; the recovered tests pass. - No duplicate `githubBaseURL`, one ref model, `docs/agent-github.md` reconciled with the `github-token.md`/`forgejo-git-auth.md` docs #489 already shipped. - Prefer `docs/agent-github.md` for docs. Do not touch `docs/FEATURES.md` front matter (pinned at its char cap, one-agent-at-a-time tonight). ## Related - ward#489 (the partial landing this completes). - ward#518 (in flight on `container_reap.go` - stay off that file). - ward#504 (the salvage issue `ward-b4d43076` was filed under). - ward#441 / ward#453 (GitHub token is env-only, never SSM - the recovered `resolveGitHubToken` already honours this). Filed by Claude (she/her) during the ward#492 burndown, recovering abandoned work at Kai's request.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-520 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-02T07:38:16Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-ward-520` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-02T07:38:16Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - recovered the gh-backed issueForge client (forge enum, gh client, ref parsing, container wiring) onto current main; build/vet/test green, landed on main.

Honest retro: the recovery itself was the easy part - the salvage branch was clean, well-commented work, and most of it dropped in verbatim (forge.go, github_ops.go, the two test files, agent-github.md). The real work was exactly where the issue said it'd be: the reconciliation. Main had grown a Base string ref model where the branch used a Forge forge enum, so I migrated .Base -> .Forge (only url() and parseAgentIssueRef read it) and kept the enum, which is genuinely the richer choice - it already drives baseURL()/gitPushUser()/client selection. I preserved main's freeform-pointer steer in parseAgentIssueRef rather than reverting to the branch's older shape.

The one thing that fought back: ward#518 landed on main while I was working, and it removed findOpenIssueByTitlePrefix from forgejoClient (the append-to-open salvage dedup). My recovered issueForge interface still listed that method, so after integrating main the build broke - *forgejoClient no longer satisfied the interface. Dropped the method from both the interface and the recovered githubClient (plus the now-dead githubListLimit), which is the correct end state anyway. Glad I integrated main before the final push and re-ran the full gate.

Confidence: high on the compile-time + unit-tested surface (ref parsing, forge selection, seed carry clause, env emission, git-cred user, token resolution - all covered and green). Lower on the truly-live path, because I can't spin a real GitHub-hosted container run from here (no external-owner trust, no live token), so the actual gh issue comment / gh pr create round-trip is exercised only through the gh verb shapes, not an end-to-end run.

Rough edges / follow-ups worth filing:

  • Reaper-side GitHub routing is deliberately still absent (per the scope boundary). Now that ward#518 has landed, container_reap.go is free - a follow-up should make reapEnv/executeReap forge-aware so a GitHub run salvages to a branch + comments the GitHub issue instead of trying to push Forgejo main. Today a GitHub run's reaper backstop is Forgejo-oriented.
  • docs/agent-github.md + AGENTS.container.md describe that eventual reaper behavior as if it exists; they're forward-looking until the reaper follow-up lands.
WARD-OUTCOME: done - recovered the gh-backed issueForge client (forge enum, `gh` client, ref parsing, container wiring) onto current main; build/vet/test green, landed on main. Honest retro: the recovery itself was the easy part - the salvage branch was clean, well-commented work, and most of it dropped in verbatim (forge.go, github_ops.go, the two test files, agent-github.md). The real work was exactly where the issue said it'd be: the reconciliation. Main had grown a `Base string` ref model where the branch used a `Forge forge` enum, so I migrated `.Base` -> `.Forge` (only `url()` and `parseAgentIssueRef` read it) and kept the enum, which is genuinely the richer choice - it already drives `baseURL()`/`gitPushUser()`/client selection. I preserved main's freeform-pointer steer in `parseAgentIssueRef` rather than reverting to the branch's older shape. The one thing that fought back: ward#518 landed on `main` *while I was working*, and it removed `findOpenIssueByTitlePrefix` from `forgejoClient` (the append-to-open salvage dedup). My recovered `issueForge` interface still listed that method, so after integrating main the build broke - `*forgejoClient` no longer satisfied the interface. Dropped the method from both the interface and the recovered `githubClient` (plus the now-dead `githubListLimit`), which is the correct end state anyway. Glad I integrated main *before* the final push and re-ran the full gate. Confidence: high on the compile-time + unit-tested surface (ref parsing, forge selection, seed carry clause, env emission, git-cred user, token resolution - all covered and green). Lower on the truly-live path, because I can't spin a real GitHub-hosted container run from here (no external-owner trust, no live token), so the actual `gh issue comment` / `gh pr create` round-trip is exercised only through the `gh` verb shapes, not an end-to-end run. Rough edges / follow-ups worth filing: - Reaper-side GitHub routing is deliberately still absent (per the scope boundary). Now that ward#518 has landed, `container_reap.go` is free - a follow-up should make `reapEnv`/`executeReap` forge-aware so a GitHub run salvages to a branch + comments the GitHub issue instead of trying to push Forgejo main. Today a GitHub run's reaper backstop is Forgejo-oriented. - docs/agent-github.md + AGENTS.container.md describe that eventual reaper behavior as if it exists; they're forward-looking until the reaper follow-up lands.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#520
No description provided.