feat(groomer): add durable history, audit UI, repo context, and guardrails#468
Conversation
…rails Close #460. Turns the hosted groomer MVP into a complete, operator-visible subsystem without broadening its scope (still no code edits, PRs, merges, clones, or shell). - GroomingRun table: dedicated history with labels/lane before+after, context warnings, failure stage, comment URL, and indexes. - Repository context: bounded GitHub REST API context (metadata, code search, file snippets) with hard caps on searches/files/bytes. No clone. - Comment cooldown: skips repeat comments within a configurable window. - History API: GET /api/groomer/runs and /api/groomer/runs/[id]. - Operator UI: /automation/groomer with status, issue, lane/label diffs, model, timestamps, and JSON detail links. - Auth: optional DISPATCH_GROOMER_TOKEN for scheduled invocations. - Docs: new env vars, history/audit, repo context, scheduling.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — routed smart (risk match: auth_changes)
Recommendation: Approve
This PR adds durable history, audit UI, repository context, and guardrails to the hosted groomer feature. The changes are additive, well-tested, and respect the safety constraints established by the original groomer MVP (issue PR 462) and the design in PR 460.
Change-by-Change Findings
Database Migration (prisma/schema.prisma)
- New
GroomingRuntable only — no modifications to existing tables. No data loss risk. The table usesCuid()IDs, cascade/set-null deletes on foreign keys, and includes composite indexes for the query patterns used by the new history API. Migration is safe.
Auth Changes (src/lib/auth.ts)
- New
authorizeGroomerRequestfunction addsDISPATCH_GROOMER_TOKENas an alternative bearer token for the groomer endpoint. It falls back to standard auth (agent token, basic, OIDC) if the groomer token is not set. ThesafeEqualcomparison is used for token comparison, which is the correct timing-safe pattern. No regression in existing auth flows —authorizeRequestis unchanged.
Path Handling (src/lib/groomer/repository-context.ts)
fetchRepositoryFileTextencodes each path segment individually withencodeURIComponent, which properly URL-encodes..,/, null bytes, and other traversal sequences before constructing the GitHub Contents API URL (/repos/{owner}/{repo}/contents/{path}). Not a filesystem path — these are HTTP requests to GitHub's API, which resolves paths server-side.isTextLikeuses an allowlist of extensions (.ts,.tsx,.js, etc.); files without extensions or with non-text extensions (.png,.bin) are skipped regardless of path content.- Repository context is disabled by default (
DISPATCH_GROOMER_REPO_CONTEXT_ENABLED=false), capped bymaxSearches,maxContextFiles,maxFileBytes, and a 40% budget ofmaxContextBytes.
New API Endpoints
GET /api/groomer/runsandGET /api/groomer/runs/[id]— both protected byauthorizeRequest, return 401 on unauthorized access, 404 on missing runs.POST /api/groomer/run— switched toauthorizeGroomerRequest, returns a generic error message on failure (intentional security improvement over leaking error details).
GroomingRun History (src/lib/groomer/history.ts)
createGroomingRunRecordmirrorslabelsBefore → labelsAfterandlaneBefore → laneAfteron creation, ensuring the "before" state is captured atomically before any mutations.listGroomingRunsclampstaketo[1, 200]range.completeGroomingRunRecordsetscompletedAtautomatically.
Guardrails Preserved
- Dry-run defaults to true.
- Comment cooldown enforcement queries prior
GroomingRunrecords byissueIdandcreatedAt. - Structured JSON schema validation via
validateGroomerOutputis applied before any side effects. - Leases are acquired and released around the run.
- Repository context failures are soft (warnings recorded, groomer continues).
Standards Compliance
- Prisma schema conventions: ✅ new model with
@id,@default(cuid()), proper relations, and composite indexes. - API route conventions: ✅ appropriate HTTP status codes (200, 401, 404, 500), JSON responses,
error instanceof Errorpattern in handlers. - Label allowlist: ✅ enforced by
validateGroomerOutputschema validation. - Audit trail: ✅
AuditLogandAgentRunstill written; newGroomingRunis the detailed drilldown. - Agent workflow contract: ✅
hosted-groomeruses the samegroomrun type; grooming scope unchanged (no PRs, no code edits, no shell).
Linked Issue Fit (PR 460)
All acceptance criteria are addressed:
- ✅ One issue per invocation with candidate selection
- ✅ Bounded repository context via GitHub REST APIs (no clone, no shell)
- ✅ Provider-neutral LLM adapter
- ✅ Schema-validated output before side effects
- ✅ Dry-run mode returns mutation plan without writes
- ✅ Write mode updates labels, adds comment (with cooldown), updates Dispatch cache
- ✅
GroomingRuntable with audit trail - ✅ External worker grooming (
next-task?mode=groom) unaffected - ✅ Unit tests cover selection, validation, dry-run, mutation planning, and failure handling
Evidence Provider Findings
No evidence providers configured for this PR.
Tool Harness Findings
No tool calls issued; reviewed corpus directly.
Required Checks
-
review migration for data loss risk — ✅ Verified.
GroomingRunis a new table added via schema delta. No existing tables are modified. UsesonDelete: Cascadefor Issue relation,onDelete: SetNullfor AgentRun. No data loss possible. -
test migration on a copy of production schema —
⚠️ Cannot verify from corpus. The migration is a straightforwardCREATE TABLEwith standard types. Recommend verifying in a staging environment before production deployment. -
review for path traversal vulnerabilities — ✅ Verified. Paths go to GitHub Contents API URL (
/repos/{owner}/{repo}/contents/{encoded_path}), not a filesystem.encodeURIComponenton each segment encodes traversal sequences. Allowlist extension filter blocks binary/non-text files. Repository context disabled by default with hard budget caps. -
test with edge-case paths (null bytes, symlinks) — ℹ️ Not explicitly covered by unit tests. Path encoding via
encodeURIComponenthandles null bytes. Symlinks are not applicable since the groomer reads from GitHub's API, not a local filesystem. The existing test suite inrepository-context.test.tscovers empty paths, no-extension files, duplicate paths, and missing content but not explicit null-byte injection. Acceptable given the API-layer protection. -
review auth flow for regression — ✅ Verified.
authorizeRequestis unchanged.authorizeGroomerRequestadds a fallback path without modifying existing token validation.safeEqualis used for groomer token comparison. -
verify session token handling is correct — ✅ Verified.
DISPATCH_GROOMER_TOKENis read from env, trimmed, and compared usingsafeEqual. It is never logged or persisted. The token only authorizes the groomer endpoint, not other Dispatch APIs.
Unknowns / Needs Verification
- Migration has not been tested on a production-schema copy — recommend a staging validation run before deploying to production.
- The
Bufferglobal used inrepository-context.tsandgithub.tsassumes Node.js runtime (consistent with the Node:24 Docker image used in the Dockerfile). No browser-specific issues detected.
Closes #460.
Turns the hosted groomer MVP (#462) into a complete, operator-visible subsystem. The groomer's scope is unchanged — it still doesn't edit code, open PRs, merge, clone repositories, or run shell.
What's added
GroomingRunhistory table — dedicated audit record with labels/lane before+after, context warnings, failure stage, comment URL, prompt/model metadata, and composite indexes.AgentRun/AuditLogare still written for existing views;GroomingRunis the detailed drilldown.DISPATCH_GROOMER_REPO_CONTEXT_ENABLED=true, the groomer gathers bounded context via GitHub REST APIs only (metadata, code search, file snippets). Hard caps on searches, files, bytes. No clone, no shell. Failures are soft and recorded as warnings.DISPATCH_GROOMER_COMMENT_COOLDOWN_HOURS(default 24), unlessforceis set.GET /api/groomer/runs(list with filters) andGET /api/groomer/runs/[id](detail)./automation/groomershows recent runs with status, issue, lane/label diffs, model, timestamps, and JSON links. Linked from the automation overview.DISPATCH_GROOMER_TOKENfor scheduled/admin invocations alongsideDISPATCH_AGENT_TOKEN.README.mdanddocs/hosted-groomer.mdupdated with new env vars, history/audit, repo context, and scheduling.Verification
npm run lint— clean (0 warnings)npm run typecheck— cleannpm test— 1797 tests pass, 100 files