Implement the IssueTracker port (#51) as a real Gitea client. This is the new outbound dependency the brain server does not have today — per the #49 ground-truth, the brain server does brain-local file ops only and knows nothing about Gitea. Isolated and testable against a Gitea test instance.
What to build
A concrete IssueTracker impl: CreateIssue(repo, title, body), CloseIssue(repo, number, comment), CommentIssue(repo, number, body). Owner is always mathias (hard-coded; never caller-supplied).
Gitea API HTTP client, injected into the brain server behind the IssueTracker interface (swappable/testable — the use-case never sees the concrete type).
Token handling: Gitea API token via env, never in argv or logs (AGENTS.md secret-handling; the 2026-06-11 base64-masking-defeat incident is the cautionary precedent). Token read once at construction.
Map Gitea responses to the Ticket receipt fields ({repo, number, action, url, ok}).
Acceptance criteria
Concrete IssueTracker implementing #51's port: create / close / comment
Owner forced to mathias; never read from caller input
Injected into the brain server behind the interface (constructor injection)
Token via env only; no secret in argv/logs; verify with a log-scrape test
Errors mapped to per-item receipt ok:false + message (best-effort; one failed ticket doesn't abort the call)
Tests against a Gitea test instance (or a recorded/faked transport): create, close, comment, error path
task check green
Notes
Decision from #49: the Gitea client lives in the brain server, injected behind the interface — not a separate composing service. This keeps capture as one use-case while keeping the dependency swappable. The interface boundary is what lets #51's use-case tests stay hermetic (fake tracker) while this issue tests the real wire.
**Parent:** #49 · **Spec:** `specs/capture-bdd-spec.md` §1 · **Depends on:** #51 (the `IssueTracker` port) · **Blocks:** #49d
## Scope
Implement the `IssueTracker` port (#51) as a real Gitea client. This is the **new outbound dependency the brain server does not have today** — per the #49 ground-truth, the brain server does brain-local file ops only and knows nothing about Gitea. Isolated and testable against a Gitea test instance.
## What to build
- A concrete `IssueTracker` impl: `CreateIssue(repo, title, body)`, `CloseIssue(repo, number, comment)`, `CommentIssue(repo, number, body)`. Owner is always `mathias` (hard-coded; never caller-supplied).
- Gitea API HTTP client, injected into the brain server behind the `IssueTracker` interface (swappable/testable — the use-case never sees the concrete type).
- **Token handling:** Gitea API token via env, **never in argv or logs** (AGENTS.md secret-handling; the 2026-06-11 base64-masking-defeat incident is the cautionary precedent). Token read once at construction.
- Map Gitea responses to the `Ticket` receipt fields (`{repo, number, action, url, ok}`).
## Acceptance criteria
- [ ] Concrete `IssueTracker` implementing #51's port: create / close / comment
- [ ] Owner forced to `mathias`; never read from caller input
- [ ] Injected into the brain server behind the interface (constructor injection)
- [ ] Token via env only; no secret in argv/logs; verify with a log-scrape test
- [ ] Errors mapped to per-item receipt `ok:false` + message (best-effort; one failed ticket doesn't abort the call)
- [ ] Tests against a Gitea test instance (or a recorded/faked transport): create, close, comment, error path
- [ ] `task check` green
## Notes
Decision from #49: the Gitea client lives **in the brain server**, injected behind the interface — not a separate composing service. This keeps capture as one use-case while keeping the dependency swappable. The interface boundary is what lets #51's use-case tests stay hermetic (fake tracker) while this issue tests the real wire.
create / comment / close(+optional closing comment) over the Gitea REST API.
Owner const "mathias" — never a param; a caller can't redirect to another owner's repo.
Token: read once from BRAIN_GITEA_TOKEN, only ever in the Authorization header — never logged/argv. TestErrorPathDoesNotLeakToken proves the token never appears in an error string. gitea.New → nil when unconfigured.
Injected via mcp.Server.WithIssueTracker (constructor injection) + IssueTracker() accessor; main wires from BRAIN_GITEA_URL (default https://git.d-ma.be) + BRAIN_GITEA_TOKEN. Consumed by the use-case in #53.
Port refinement:CloseIssue gained a comment param to match this issue's spec; service passes the ticket body as the closing comment.
Deploy note: 1Password DMABE_GITEA_API_TOKEN → maps to BRAIN_GITEA_TOKEN in the k3s manifest (#55/infra). Secret never in repo/argv/logs.
All ACs met (httptest-backed: create/comment/close±comment/error/auth-header/owner-forcing); task check green.
Critical path next: #53 — POST /capture REST adapter + OAuth2 + the I1 sovereignty gate (server-derived origin). All three converging deps (#50 classification, #51 service, #52 tracker) are now in flight; #53 wires them together and adds the origin gate. After #53 = "capture works" for direct-REST harnesses.
## Implemented — PR #58 (open, stacked on #57)
**PR:** https://git.d-ma.be/mathias/hyperguild/pulls/58 · base `feat/capture-service` (#52-only diff). Merge order: **#56 → #57 → #58**.
`ingestion/internal/gitea/` — `Client` implementing `capture.IssueTracker`:
- create / comment / close(+optional closing comment) over the Gitea REST API.
- **Owner const `"mathias"`** — never a param; a caller can't redirect to another owner's repo.
- **Token:** read once from `BRAIN_GITEA_TOKEN`, only ever in the `Authorization` header — never logged/argv. `TestErrorPathDoesNotLeakToken` proves the token never appears in an error string. `gitea.New` → nil when unconfigured.
- Injected via `mcp.Server.WithIssueTracker` (constructor injection) + `IssueTracker()` accessor; `main` wires from `BRAIN_GITEA_URL` (default `https://git.d-ma.be`) + `BRAIN_GITEA_TOKEN`. Consumed by the use-case in #53.
**Port refinement:** `CloseIssue` gained a `comment` param to match this issue's spec; service passes the ticket body as the closing comment.
**Deploy note:** 1Password `DMABE_GITEA_API_TOKEN` → maps to `BRAIN_GITEA_TOKEN` in the k3s manifest (#55/infra). Secret never in repo/argv/logs.
All ACs met (`httptest`-backed: create/comment/close±comment/error/auth-header/owner-forcing); `task check` green.
**Critical path next: #53** — `POST /capture` REST adapter + OAuth2 + the I1 sovereignty gate (server-derived origin). All three converging deps (#50 classification, #51 service, #52 tracker) are now in flight; #53 wires them together and adds the origin gate. After #53 = "capture works" for direct-REST harnesses.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Parent: #49 · Spec:
specs/capture-bdd-spec.md§1 · Depends on: #51 (theIssueTrackerport) · Blocks: #49dScope
Implement the
IssueTrackerport (#51) as a real Gitea client. This is the new outbound dependency the brain server does not have today — per the #49 ground-truth, the brain server does brain-local file ops only and knows nothing about Gitea. Isolated and testable against a Gitea test instance.What to build
IssueTrackerimpl:CreateIssue(repo, title, body),CloseIssue(repo, number, comment),CommentIssue(repo, number, body). Owner is alwaysmathias(hard-coded; never caller-supplied).IssueTrackerinterface (swappable/testable — the use-case never sees the concrete type).Ticketreceipt fields ({repo, number, action, url, ok}).Acceptance criteria
IssueTrackerimplementing #51's port: create / close / commentmathias; never read from caller inputok:false+ message (best-effort; one failed ticket doesn't abort the call)task checkgreenNotes
Decision from #49: the Gitea client lives in the brain server, injected behind the interface — not a separate composing service. This keeps capture as one use-case while keeping the dependency swappable. The interface boundary is what lets #51's use-case tests stay hermetic (fake tracker) while this issue tests the real wire.
Implemented — PR #58 (open, stacked on #57)
PR: #58 · base
feat/capture-service(#52-only diff). Merge order: #56 → #57 → #58.ingestion/internal/gitea/—Clientimplementingcapture.IssueTracker:"mathias"— never a param; a caller can't redirect to another owner's repo.BRAIN_GITEA_TOKEN, only ever in theAuthorizationheader — never logged/argv.TestErrorPathDoesNotLeakTokenproves the token never appears in an error string.gitea.New→ nil when unconfigured.mcp.Server.WithIssueTracker(constructor injection) +IssueTracker()accessor;mainwires fromBRAIN_GITEA_URL(defaulthttps://git.d-ma.be) +BRAIN_GITEA_TOKEN. Consumed by the use-case in #53.Port refinement:
CloseIssuegained acommentparam to match this issue's spec; service passes the ticket body as the closing comment.Deploy note: 1Password
DMABE_GITEA_API_TOKEN→ maps toBRAIN_GITEA_TOKENin the k3s manifest (#55/infra). Secret never in repo/argv/logs.All ACs met (
httptest-backed: create/comment/close±comment/error/auth-header/owner-forcing);task checkgreen.Critical path next: #53 —
POST /captureREST adapter + OAuth2 + the I1 sovereignty gate (server-derived origin). All three converging deps (#50 classification, #51 service, #52 tracker) are now in flight; #53 wires them together and adds the origin gate. After #53 = "capture works" for direct-REST harnesses.