Triage Rules
A triage rule matches an issue title against apattern (substring, regex, or exact) and applies a bundle of updates: pick a crew, set an assignee, set priority, project, and labels. Rules are evaluated in position order; the first matching rule wins per issue (other rules don’t apply, even if they’d also match).
Implementation: internal/api/triage_handler.go. Backed by the triage_rules table.
All endpoints require an authenticated session and workspace context.
Endpoints
Rule shape
Rule CRUD
Manage the ordered rule set. Rules evaluate inposition order; first match wins.
GET /api/v1/triage-rules
List every rule in the workspace, ordered by position ASC, created_at ASC.
Request: No request body or query parameters.
Auth: authenticated session + workspace context.
Response: 200 OK — JSON array (never null).
POST /api/v1/triage-rules
Create a new rule. Auto-positioned at the end of the queue (MAX(position) + 1); reorder with PATCH.
Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role (requireRole("create")).
Request body:
Rules are created
enabled = true and match_count = 0.
Response: 201 Created with the rule object (same shape as List).
WebSocket event: triage_rule.created broadcast on the workspace channel with { "id": "<rule id>" }.
PATCH /api/v1/triage-rules/{ruleId}
Partial update. Every field optional. Pass crew_id, assignee_id, or project_id as "" to NULL them out (the handler distinguishes “field absent” from “field set to empty string” and routes the empty string to SET column = NULL).
Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role.
Request body:
When both
pattern and match_type are present and match_type is regex, the new pattern is recompiled — invalid regex → 400. If only one of the two is supplied, no recompile happens, so changing match_type from contains → regex without also resending the pattern can leave the rule with an unvalidated regex; the runtime Process path catches that and skips the rule rather than aborting the batch.
Response: 200 OK with the full updated rule object.
WebSocket event: triage_rule.updated broadcast on the workspace channel.
DELETE /api/v1/triage-rules/{ruleId}
Hard delete. Higher role bar than create / update because the rule is the source of truth for downstream automation.
Request: No request body. The {ruleId} path parameter identifies the rule to delete.
Auth: authenticated session + workspace context + OWNER or ADMIN role (requireRole("manage")).
Response: 204 No Content.
WebSocket event: triage_rule.deleted broadcast on the workspace channel.
Process the backlog
One-shot endpoint that applies the rule set to the current backlog.POST /api/v1/triage/process
Run all enabled rules against every unassigned BACKLOG issue in the workspace and apply the first-match update to each. Issues are selected with assignee_id IS NULL; a rerun therefore skips issues that received an assignee, but an issue matched by a rule that does not set assignee_id can be processed again.
Request: JSON object {}; no path or query parameters.
Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role (requireRole("create")).
Algorithm
- Load enabled rules ordered by
position ASC. Regex patterns are pre-compiled once per call (so a single bad regex in the rule set logs a warning and skips that rule, but doesn’t abort the batch or re-compile per issue). - Load all BACKLOG missions where
assignee_id IS NULLandmission_type = 'issue'(sub-issues, missions, and crew missions are excluded). - For each issue, walk the rule list and apply the first matching rule — set
crew_id,assignee_id(withassignee_type = 'agent'),priority, andproject_idfrom the rule. Subsequent rules are skipped for that issue even if they’d also match. - Increment
match_counton rules that matched. - Broadcast
triage.processedon the workspace channel withprocessed(total backlog scanned) andmatched(rows mutated) — only when at least one match occurred.
Match-type semantics
labels_json is currently read but not applied to the issue inside Process — only crew_id, assignee_id/assignee_type, priority, and project_id are written. The column exists on the rule for forwards compatibility.
Response
If no rules are enabled the response is still
200 with { "processed": 0, "matched": 0 } (no SQL is run against missions).
See also
- Issues — the missions table
Processupdates. - Crews —
crew_idtargets a crew row. - Agents —
assignee_idis an agent id. - Recurring Issues — cron-driven issue creation; triage rules run on whatever lands in the backlog (manual or recurring).