feat(tracker): agent triage surface — issue create, label ops, close (ADR-0014) #4

Merged
dominik.polakovics merged 1 commit from feat/agent-triage-surface into main 2026-07-06 22:22:30 +02:00

Implements the PRD in #3: the agent surface now covers the full triage set end-to-end — labctl → agent API → Tracker seam → both bindings.

What's in here

  • Seam (internal/tracker): CreateIssue (labels at creation), AddIssueLabels, RemoveIssueLabels, Labels, EnsureLabel, plus the ErrUnknownLabel sentinel. Callers speak label names only; strict resolution on both bindings — a typo is a loud 400, never an implicit label create. The metrics decorator forwards the new ops (create_issue, label_add, label_remove, labels, label_ensure) and keeps forwarding capability interfaces (forwarding regression test extended).
  • Forgejo binding: name→ID resolution inside the client; EnsureLabel is list-first with a re-list on the forge's duplicate answer (tested against the 409 race); colors normalized to #rrggbb; existing pagination/label-verification rules untouched.
  • Builtin binding: label ops over the existing labels/issue_labels tables. Migration 0002 adds issues.author_kind/run_id mirroring issue_comments — the PRD asks for run attribution on created issues ("like comments today"), and the issues table had no author columns, so this is the one schema change. Run identity flows through the same RunScoper seam as comments.
  • Agent API: POST /issues, POST|DELETE /issues/{n}/labels (label set in the body — names may contain /), POST /issues/{n}/close, GET|POST /labels (idempotent ensure, 200 with the label that exists afterwards). Incogni sanitization widened to every agent-authored body (issue, comment, PR/CR). Builtin mutations publish issue.changed so the operator UI refetches, mirroring the existing cr.changed.
  • labctl: issue create --title --body [--labels], issue label add|remove <n> <a,b>, issue close <n>, label list, label create --name [--color --description]; issue list gains created-at + labels columns (triage buckets from one call). Exit codes unchanged (0/1/2).
  • Docs: ADR-0014 (revised invariant: engine-initiated tracker writes stay dead; deliberate agent workflow actions flow through the seam), ADR-0009 status pointer, brief D10/§8.2/§8.3, ops.md (metrics ops, incogni measure 3, triage-vs-incogni note), README, seeded CLAUDE.local.md.

Verification

Full suite + golangci-lint green. Also verified against the real binaries: seeded a builtin repo + active run, started bin/lab, and drove every new labctl command over the socket — five seeded labels listed, idempotent label create returns the original on retry, issue created with labels and stored author_kind=run, label add/remove swaps roles, comment-then-close flow, typo'd label → exit 1 naming it, usage errors → exit 2, bad token → opaque 401.

Closes #3

🤖 Generated with Claude Code

Implements the PRD in #3: the agent surface now covers the full triage set end-to-end — labctl → agent API → Tracker seam → both bindings. ## What's in here - **Seam** (`internal/tracker`): `CreateIssue` (labels at creation), `AddIssueLabels`, `RemoveIssueLabels`, `Labels`, `EnsureLabel`, plus the `ErrUnknownLabel` sentinel. Callers speak label names only; strict resolution on both bindings — a typo is a loud 400, never an implicit label create. The metrics decorator forwards the new ops (`create_issue`, `label_add`, `label_remove`, `labels`, `label_ensure`) and keeps forwarding capability interfaces (forwarding regression test extended). - **Forgejo binding**: name→ID resolution inside the client; `EnsureLabel` is list-first with a re-list on the forge's duplicate answer (tested against the 409 race); colors normalized to `#rrggbb`; existing pagination/label-verification rules untouched. - **Builtin binding**: label ops over the existing `labels`/`issue_labels` tables. **Migration 0002** adds `issues.author_kind`/`run_id` mirroring `issue_comments` — the PRD asks for run attribution on created issues ("like comments today"), and the issues table had no author columns, so this is the one schema change. Run identity flows through the same `RunScoper` seam as comments. - **Agent API**: `POST /issues`, `POST|DELETE /issues/{n}/labels` (label set in the body — names may contain `/`), `POST /issues/{n}/close`, `GET|POST /labels` (idempotent ensure, 200 with the label that exists afterwards). Incogni sanitization widened to **every** agent-authored body (issue, comment, PR/CR). Builtin mutations publish `issue.changed` so the operator UI refetches, mirroring the existing `cr.changed`. - **labctl**: `issue create --title --body [--labels]`, `issue label add|remove <n> <a,b>`, `issue close <n>`, `label list`, `label create --name [--color --description]`; `issue list` gains created-at + labels columns (triage buckets from one call). Exit codes unchanged (0/1/2). - **Docs**: ADR-0014 (revised invariant: engine-initiated tracker writes stay dead; deliberate agent workflow actions flow through the seam), ADR-0009 status pointer, brief D10/§8.2/§8.3, ops.md (metrics ops, incogni measure 3, triage-vs-incogni note), README, seeded CLAUDE.local.md. ## Verification Full suite + golangci-lint green. Also verified against the real binaries: seeded a builtin repo + active run, started `bin/lab`, and drove every new `labctl` command over the socket — five seeded labels listed, idempotent `label create` returns the original on retry, issue created with labels and stored `author_kind=run`, label add/remove swaps roles, comment-then-close flow, typo'd label → exit 1 naming it, usage errors → exit 2, bad token → opaque 401. Closes #3 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(tracker): agent triage surface — issue create, label ops, close (ADR-0014)
Some checks failed
ci / flake-check (pull_request) Has been cancelled
d894efbd0c
Extend the agent surface end-to-end (labctl → agent API → Tracker seam →
both bindings) with the full triage set: issue create with labels attached
at creation, issue label add/remove, label list, idempotent label create
(ensure), and issue close.

- Seam: CreateIssue/AddIssueLabels/RemoveIssueLabels/Labels/EnsureLabel +
  ErrUnknownLabel; callers speak label names only; the metrics decorator
  forwards the new ops into the (binding, op, result) vocabulary.
- Forgejo: name-to-ID resolution stays behind the seam; strict apply (no
  implicit label create); list-first ensure with a re-list on the forge's
  duplicate answer; colors normalized to #rrggbb.
- Builtin: label ops over the existing tables; migration 0002 adds
  issues.author_kind/run_id (mirroring issue_comments) so run-created
  issues carry run attribution through the same RunScoper seam.
- Agent API: repo-scoped triage routes; unknown label → 400 naming it;
  incogni sanitization widened to every agent-authored body (issue,
  comment, PR/CR); builtin mutations publish issue.changed.
- labctl: issue create/label add/label remove/close, label list/create;
  issue list gains created-at + labels columns; exit codes unchanged.
- Docs: ADR-0014 records the revised invariant (engine writes stay dead;
  deliberate agent workflow actions flow through the seam); ADR-0009 gets
  a status pointer; brief D10/§8.2/§8.3, ops.md metrics + incogni notes,
  README, and the seeded CLAUDE.local.md document the new surface.

Closes #3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

This was generated by AI while landing a PR.

Land-PR audit — PR #4 (head d894efb)

Checked

  • Gate: open, not draft, no conflicts, base main; Conventional-Commits title; working Closes #3.
  • Verification signal: PR CI (ci / flake-check) sat at "Waiting to run" for the whole validation (runner not picking up jobs — older runs are waiting/cancelled too), so nothing vouched. Ran the project's own checks locally on the exact head commit instead: go build ./..., go test ./... (26 packages, all ok), golangci-lint run (0 issues). The diff touches no web/ source, so the vitest half of flake-check is unaffected.
  • Review: 8 independent angles (line-by-line, removed-behavior, cross-file trace, reuse, simplification, efficiency, altitude, CLAUDE.md conventions), one adversarial verifier per surviving candidate.

Findings — none blocking (edge-case races, error-mapping divergences, dedup cleanups):

  1. CONFIRMED internal/tracker/builtin/builtin.go:206 — Add/RemoveIssueLabels loop one committed tx per label; a mid-loop failure (label deleted concurrently) commits a partial change, surfaces as 404 "not found" (reads as issue-gone, contradicting the handler's own 400-naming-the-label contract), and skips issue.changed. Atomic precedents exist in the same diff (CreateIssueWithLabels, store.SetIssueLabels).
  2. CONFIRMED internal/agentapi/handlers.go:543 — label color is never validated: builtin stores garbage verbatim (200) while the forge's 422 surfaces as 502, turning a permanent caller typo into a retry-worthy "outage" under the retry-safe ensure contract.
  3. CONFIRMED internal/agentapi/handlers.go:339 — comment "body is required" checks the raw body, sanitization runs after; an attribution-only body on an incogni builtin repo stores an empty comment (sanitize-to-empty is pinned by sanitize_test.go).
  4. CONFIRMED internal/agentapi/handlers.go:343 — comment create is the only builtin agent mutation that skips publishIssueChanged (operator comment create publishes; IssueDetail refetches on it).
  5. PLAUSIBLE internal/tracker/forgejo/forgejo.go:392 — label resolution is repo-scoped only; an org-level label an issue visibly carries can't be added/removed by name, and EnsureLabel would mint a shadowing repo-level duplicate (read/write asymmetry new in this diff).
  6. PLAUSIBLE internal/tracker/forgejo/forgejo.go:354 — Gitea-lineage forges likely answer 201 to duplicate-name label POSTs (no uniqueness check upstream), so the 409/422 recovery never fires and concurrent first-ensures can mint duplicate labels; sequential idempotency holds.
  7. CLEANUP internal/tracker/builtin/builtin.go:278 — strict name→ID resolve-and-dedupe now exists in three parallel copies (httpapi/issues.go:654, builtin.go:278, forgejo.go:392); the GitHub binding (#1) would be a fourth. A generic helper in tracker is import-cycle-free.
  8. CLEANUP internal/tracker/forgejo/forgejo.go:262defaultLabelColor re-declares store.LabelDefaultColor, kept equal only by a comment; forgejo importing store is cycle-free.

Refuted along the way: "issue titles bypass the incogni sanitizer" (body-only is the documented ADR-0014 contract and the line-stripper's design) and "forgejo per-label DELETE remove is unsafe" (the only subset-remove the API offers; idempotent under retry).

Verdict: PASS — merge gated on the maintainer's explicit confirmation.

> *This was generated by AI while landing a PR.* **Land-PR audit — PR #4** (head `d894efb`) **Checked** - Gate: open, not draft, no conflicts, base `main`; Conventional-Commits title; working `Closes #3`. - Verification signal: PR CI (`ci / flake-check`) sat at "Waiting to run" for the whole validation (runner not picking up jobs — older runs are waiting/cancelled too), so nothing vouched. Ran the project's own checks locally on the exact head commit instead: `go build ./...`, `go test ./...` (26 packages, all ok), `golangci-lint run` (0 issues). The diff touches no `web/` source, so the vitest half of flake-check is unaffected. - Review: 8 independent angles (line-by-line, removed-behavior, cross-file trace, reuse, simplification, efficiency, altitude, CLAUDE.md conventions), one adversarial verifier per surviving candidate. **Findings — none blocking** (edge-case races, error-mapping divergences, dedup cleanups): 1. CONFIRMED `internal/tracker/builtin/builtin.go:206` — Add/RemoveIssueLabels loop one committed tx per label; a mid-loop failure (label deleted concurrently) commits a partial change, surfaces as 404 "not found" (reads as issue-gone, contradicting the handler's own 400-naming-the-label contract), and skips `issue.changed`. Atomic precedents exist in the same diff (`CreateIssueWithLabels`, `store.SetIssueLabels`). 2. CONFIRMED `internal/agentapi/handlers.go:543` — label color is never validated: builtin stores garbage verbatim (200) while the forge's 422 surfaces as 502, turning a permanent caller typo into a retry-worthy "outage" under the retry-safe ensure contract. 3. CONFIRMED `internal/agentapi/handlers.go:339` — comment "body is required" checks the raw body, sanitization runs after; an attribution-only body on an incogni builtin repo stores an empty comment (sanitize-to-empty is pinned by `sanitize_test.go`). 4. CONFIRMED `internal/agentapi/handlers.go:343` — comment create is the only builtin agent mutation that skips `publishIssueChanged` (operator comment create publishes; IssueDetail refetches on it). 5. PLAUSIBLE `internal/tracker/forgejo/forgejo.go:392` — label resolution is repo-scoped only; an org-level label an issue visibly carries can't be added/removed by name, and EnsureLabel would mint a shadowing repo-level duplicate (read/write asymmetry new in this diff). 6. PLAUSIBLE `internal/tracker/forgejo/forgejo.go:354` — Gitea-lineage forges likely answer 201 to duplicate-name label POSTs (no uniqueness check upstream), so the 409/422 recovery never fires and concurrent first-ensures can mint duplicate labels; sequential idempotency holds. 7. CLEANUP `internal/tracker/builtin/builtin.go:278` — strict name→ID resolve-and-dedupe now exists in three parallel copies (`httpapi/issues.go:654`, `builtin.go:278`, `forgejo.go:392`); the GitHub binding (#1) would be a fourth. A generic helper in `tracker` is import-cycle-free. 8. CLEANUP `internal/tracker/forgejo/forgejo.go:262` — `defaultLabelColor` re-declares `store.LabelDefaultColor`, kept equal only by a comment; `forgejo` importing `store` is cycle-free. Refuted along the way: "issue titles bypass the incogni sanitizer" (body-only is the documented ADR-0014 contract and the line-stripper's design) and "forgejo per-label DELETE remove is unsafe" (the only subset-remove the API offers; idempotent under retry). **Verdict: PASS** — merge gated on the maintainer's explicit confirmation.
dominik.polakovics deleted branch feat/agent-triage-surface 2026-07-06 22:22:30 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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
Cloonar/coding-lab!4
No description provided.