feat: agent issues — cross-project issue tracking for AI agents#133
Conversation
Issue: 15 fields with CHECK constraints on status/priority, composite index (target_project, status), source_project index. IssueComment: FK to issues, composite index (issue_id, created_at). Lifecycle: open → acknowledged → resolved ⟲ reopened.
- CreateIssue with defaults (open, medium) - ListIssues with priority ordering, comment count, pagination - GetIssue with comments thread - UpdateIssueStatus with timestamp tracking - AddComment with issue updated_at refresh - AcknowledgeIssues bulk open→acknowledged - ReopenIssue with resolved-state validation and optional comment
- handleListIssues (GET /api/issues) with project/status/limit/offset
- handleGetIssue (GET /api/issues/{id}) with comments
- handleCreateIssue (POST /api/issues) with validation
- handleUpdateIssue (PATCH /api/issues/{id}) status + comment
- handleAcknowledgeIssues (POST /api/issues/acknowledge) bulk
- formatIssuesForInjection for session-start hook
- IssueStore initialized and wired in Service
- 5 routes registered in requireReady group
- tools_issues.go: create/list/get/update/comment/reopen handlers - server.go: issues tool registered with full input schema - service.go: issueStore wired into MCP server via SetIssueStore - Auto-fills source_project and source_agent from session context - Human-readable text output for all actions
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughДобавлена подсистема трекинга issues: миграция и GORM-модели, новый GORM-backed IssueStore с CRUD и переходами статусов, MCP-инструмент, HTTP API-эндпойнты, интеграция в worker/service, фронтенд (список/детали) и плагинная инъекция/авто-acknowledge. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant HTTP as "HTTP Handler"
participant Service
participant Store as "IssueStore (GORM)"
participant DB as "Postgres"
Client->>HTTP: POST /api/issues {title,...}
HTTP->>Service: CreateIssue request
Service->>Store: CreateIssue(ctx, issue)
Store->>DB: INSERT INTO issues
DB-->>Store: new id
Store-->>Service: id
Service-->>HTTP: 201 {id,message}
HTTP-->>Client: 201
Client->>HTTP: GET /api/issues?project=X&status=open,reopened
HTTP->>Service: ListIssues request
Service->>Store: ListIssues(ctx, project, statuses, limit, offset)
Store->>DB: SELECT issues with correlated comment_count subquery (ORDER BY priority CASE, created_at DESC)
DB-->>Store: rows + total
Store-->>Service: []IssueWithCount, total
Service-->>HTTP: 200 {issues,total}
HTTP-->>Client: 200
Client->>HTTP: PATCH /api/issues/{id} {status: "resolved", comment: "..."}
HTTP->>Service: Update request
Service->>Store: UpdateIssueStatus(ctx, id, "resolved")
Store->>DB: UPDATE issues SET status='resolved', resolved_at=now(), updated_at=now() WHERE id=? AND ...guard...
DB-->>Store: rows affected
alt comment present
Service->>Store: AddComment(ctx, id, comment)
Store->>DB: INSERT INTO issue_comments
DB-->>Store: comment id
end
Store-->>Service: result
Service-->>HTTP: 200 {message}
HTTP-->>Client: 200
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.11.4)Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- lib.js: formatIssuesBlock() renders <open-issues> XML block with priority ordering, comment count, time-ago formatting - session-start.js: fetches GET /api/issues for current project, renders block before memory, auto-acknowledges open issues - Plugin version 3.3.0 → 3.4.0
There was a problem hiding this comment.
Code Review
This pull request introduces a cross-project issue tracking system for agents, enabling them to create, list, and manage issues across different projects. The implementation includes a new IssueStore for GORM-based persistence, database migrations for issues and issue_comments tables, and integration into the MCP server and worker service via new tool definitions and HTTP API endpoints. Review feedback identifies several areas for improvement: a type mismatch between the Labels model field and the JSONB database column, missing field definitions in the MCP tool's input schema, inconsistent metadata retrieval in API handlers, a potential bug in status string parsing, and a race condition in the issue reopening logic.
| TargetProject string `gorm:"type:text;not null;index:idx_issues_target_status,priority:1"` | ||
| SourceAgent string `gorm:"type:text"` | ||
| CreatedBySession string `gorm:"type:text"` | ||
| Labels pq.StringArray `gorm:"type:jsonb;default:'[]'"` |
There was a problem hiding this comment.
There is a type mismatch between the GORM model and the database schema for the Labels field. The migration defines this column as JSONB, but pq.StringArray is designed for PostgreSQL native arrays (text[]). Using pq.StringArray with a JSONB column will cause runtime errors during persistence because it serializes to the PostgreSQL array format (e.g., {val1,val2}) instead of JSON format (e.g., ["val1","val2"]).
| Labels pq.StringArray `gorm:"type:jsonb;default:'[]'"` | |
| Labels models.JSONStringArray `gorm:"type:jsonb;default:'[]'"` |
| "properties": map[string]any{ | ||
| "action": map[string]any{"type": "string", "enum": []string{"create", "list", "get", "update", "comment", "reopen"}, "description": "Action to perform"}, | ||
| "title": map[string]any{"type": "string", "description": "Issue title (required for create)"}, | ||
| "body": map[string]any{"type": "string", "description": "Issue body or comment text"}, | ||
| "priority": map[string]any{"type": "string", "enum": []string{"critical", "high", "medium", "low"}, "default": "medium"}, | ||
| "target_project": map[string]any{"type": "string", "description": "Target project slug (defaults to current project)"}, | ||
| "labels": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "Labels (bug, feature, etc.)"}, | ||
| "id": map[string]any{"type": "integer", "description": "Issue ID (required for get/update/comment/reopen)"}, | ||
| "status": map[string]any{"type": "string", "enum": []string{"resolved"}, "description": "Set status (only 'resolved' via update)"}, | ||
| "comment": map[string]any{"type": "string", "description": "Comment to add with status change or reopen"}, | ||
| "project": map[string]any{"type": "string", "description": "Filter by project (for list action)"}, | ||
| }, |
There was a problem hiding this comment.
The InputSchema for the issues tool is missing definitions for fields used in the implementation. Specifically, agent_source is used in handleIssueCreate, handleIssueUpdate, and others, but is not defined here. Additionally, the project property is described as being only for the list action, but it is also used as the source_project during issue creation. These should be explicitly defined so that AI agents know to provide them.
| sourceProject := r.URL.Query().Get("source_project") | ||
| sourceAgent := r.URL.Query().Get("source_agent") |
There was a problem hiding this comment.
In handleUpdateIssue (PATCH), the author metadata (source_project and source_agent) is being retrieved from query parameters. This is inconsistent with handleCreateIssue (POST), where similar metadata is expected in the JSON request body. It is better to include these fields in the JSON body for the PATCH request as well to maintain a consistent API design.
| statuses := strings.Split(statusParam, ",") | ||
| for i := range statuses { | ||
| statuses[i] = strings.TrimSpace(statuses[i]) | ||
| } |
There was a problem hiding this comment.
The status parameter parsing in handleIssueList can lead to incorrect database queries if the input string contains trailing commas or is empty. strings.Split on an empty string returns a slice containing one empty string ([""]), which will cause the store to query for issues with an empty status string. It is safer to filter out empty strings after splitting.
| statuses := strings.Split(statusParam, ",") | |
| for i := range statuses { | |
| statuses[i] = strings.TrimSpace(statuses[i]) | |
| } | |
| statuses := []string{} | |
| for _, s := range strings.Split(statusParam, ",") { | |
| if trimmed := strings.TrimSpace(s); trimmed != "" { | |
| statuses = append(statuses, trimmed) | |
| } | |
| } |
| if err := tx.Model(&Issue{}).Where("id = ?", id).Updates(map[string]interface{}{ | ||
| "status": "reopened", | ||
| "reopened_at": now, | ||
| "updated_at": now, | ||
| }).Error; err != nil { |
There was a problem hiding this comment.
The ReopenIssue method has a potential race condition. While it checks the status at line 198, a concurrent request could change the status between that check and the Updates call at line 204. To ensure atomicity, the status check should be part of the Where clause in the update statement.
result := tx.Model(&Issue{}).Where("id = ? AND status = ?", id, "resolved").Updates(map[string]interface{}{
"status": "reopened",
"reopened_at": now,
"updated_at": now,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("issue %d not found or not in resolved state", id)
}- api.ts: Issue/IssueComment types, fetchIssues, fetchIssue, acknowledgeIssues - useIssues.ts: reactive composable with status/project filters - IssuesView.vue: card list with priority badges, status colors, filters - IssueDetailView.vue: full issue with chronological timeline (events + comments) - Router: /issues and /issues/:id routes - Sidebar: "Issues" nav item between Patterns and Sessions
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/db/gorm/issue_store.go`:
- Around line 150-155: Текущая транзакция вставляет созданный комментарий
(переменная created через tx.Create) и затем делает tx.Model(&Issue{}).Where("id
= ?", issueID).Update("updated_at", now) без проверки RowsAffected, поэтому при
отсутствии родительского issue комментарий коммитится как orphan; нужно
проверить результат UPDATE и вернуть ошибку, если RowsAffected == 0 (например
вернуть gorm.ErrRecordNotFound или кастомную ошибку) из анонимной функции
транзакции, чтобы транзакция откатилась и вставка не сохранилась.
- Around line 28-48: В методе CreateIssue в месте формирования created (функция
CreateIssue, тип Issue) нужно валидировать поля Status и Priority до вставки:
проверять что значения входят в допустимые множества (например const/var с
allowedStatuses и allowedPriorities) и при некорректном значении возвращать
валидативную ошибку (например error типа validation error с понятным сообщением)
вместо попытки записать в БД; если поле пустое — подставлять дефолт
("open"/"medium") как сейчас, но любые неизвестные строки отклонять и не
вызывать INSERT/DB чек-констрейнт.
In `@internal/db/gorm/models.go`:
- Around line 430-437: The Labels field currently uses pq.StringArray which maps
to PostgreSQL text[] and conflicts with the gorm tag type:jsonb; change the
Labels field type to models.JSONStringArray (the JSON-aware type used elsewhere)
and keep the gorm tag as type:jsonb;default:'[]' so reads/writes align; update
any code paths that assume pq.StringArray (e.g., CreateIssue/GetIssue handling
of Labels) to use models.JSONStringArray serialization/deserialization where
necessary.
In `@internal/mcp/server.go`:
- Around line 967-987: The "issues" tool is being registered in the local tools
block but not included in the actual discovery response returned by the method
that currently returns "primary" (tools/list), so clients won't see it; change
the discovery-returning method to include the "issues" entry in the returned
tools set only when s.issueStore != nil (i.e., gate publishing of the issues
tool on s.issueStore != nil) and ensure the tool is added to the actual returned
collection used by tools/list (not just the local "primary" variable) so MCP
clients can discover it.
In `@internal/mcp/tools_issues.go`:
- Line 1: Rename the file internal/mcp/tools_issues.go to follow the numbered
MCP-tools pattern (e.g., internal/mcp/tools_01_issues.go) and update any
references to that file name in the repo (build scripts, go:generate directives,
or tests) so the new filename is used; ensure the package declaration (package
mcp) and all symbols defined inside remain unchanged so compilation behavior
does not change.
In `@internal/worker/handlers_issues.go`:
- Around line 221-231: The XML-like output in handlers_issues.go builds strings
with unescaped values (project, issue.SourceProject, issue.Title) which allows
injection; update the code where the strings.Builder is populated (the initial
sb.WriteString for "<open-issues ...>" and the per-issue sb.WriteString lines)
to escape values for the proper XML context—use a standard escape function
(e.g., html.EscapeString or xml.Escape) on project and issue.SourceProject when
used inside the attribute and on issue.Title (and any other text nodes) when
used in text content; ensure attribute values are quoted after escaping so
quotes/&/</> are neutralized before writing to sb.
In `@internal/worker/service.go`:
- Line 1086: The MCP server keeps a stale issue store after
reinitializeDatabase() recreates s.issueStore because SetIssueStore() is only
called during initial setup; update the MCP server to point to the new store
after reinitialization by calling mcpServer.SetIssueStore(s.issueStore) (or
equivalent setter) immediately after s.issueStore is replaced and before closing
the oldStore, and mirror the same fix for the other occurrence mentioned (around
the second spot where SetIssueStore is only called once). Ensure references to
mcpServer, SetIssueStore, reinitializeDatabase, s.issueStore and
oldStore.Close() are updated so handlers use the new store.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d48af92b-0354-4f39-bb24-f1bf79f155f6
📒 Files selected for processing (7)
internal/db/gorm/issue_store.gointernal/db/gorm/migrations.gointernal/db/gorm/models.gointernal/mcp/server.gointernal/mcp/tools_issues.gointernal/worker/handlers_issues.gointernal/worker/service.go
| { | ||
| Name: "issues", | ||
| Description: "Create, track, and resolve cross-project issues between agents. Issues are automatically shown to agents working on the target project. Use to report bugs, request features, or leave notes for agents in other projects.", | ||
| tier: tierCore, | ||
| InputSchema: map[string]any{ | ||
| "type": "object", | ||
| "required": []string{"action"}, | ||
| "properties": map[string]any{ | ||
| "action": map[string]any{"type": "string", "enum": []string{"create", "list", "get", "update", "comment", "reopen"}, "description": "Action to perform"}, | ||
| "title": map[string]any{"type": "string", "description": "Issue title (required for create)"}, | ||
| "body": map[string]any{"type": "string", "description": "Issue body or comment text"}, | ||
| "priority": map[string]any{"type": "string", "enum": []string{"critical", "high", "medium", "low"}, "default": "medium"}, | ||
| "target_project": map[string]any{"type": "string", "description": "Target project slug (defaults to current project)"}, | ||
| "labels": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "Labels (bug, feature, etc.)"}, | ||
| "id": map[string]any{"type": "integer", "description": "Issue ID (required for get/update/comment/reopen)"}, | ||
| "status": map[string]any{"type": "string", "enum": []string{"resolved"}, "description": "Set status (only 'resolved' via update)"}, | ||
| "comment": map[string]any{"type": "string", "description": "Comment to add with status change or reopen"}, | ||
| "project": map[string]any{"type": "string", "description": "Filter by project (for list action)"}, | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
issues сейчас не попадёт в tools/list.
Этот блок добавляется в tools, но на Line 1481 метод возвращает primary, а не tools, поэтому MCP-клиенты не увидят новый инструмент через discovery. Если issues должен быть доступен, его нужно добавить в реально возвращаемый набор и публиковать только при s.issueStore != nil.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcp/server.go` around lines 967 - 987, The "issues" tool is being
registered in the local tools block but not included in the actual discovery
response returned by the method that currently returns "primary" (tools/list),
so clients won't see it; change the discovery-returning method to include the
"issues" entry in the returned tools set only when s.issueStore != nil (i.e.,
gate publishing of the issues tool on s.issueStore != nil) and ensure the tool
is added to the actual returned collection used by tools/list (not just the
local "primary" variable) so MCP clients can discover it.
| @@ -0,0 +1,234 @@ | |||
| package mcp | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Переименуйте файл по нумерованному шаблону MCP-tools.
tools_issues.go выбивается из принятой схемы именования; для новых MCP-обработчиков здесь нужен нумерованный файл вида tools_<nn>_issues.go.
As per coding guidelines, All MCP tool handlers (48 tools) must be implemented with numbered files (tools_*.go) in internal/mcp/.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcp/tools_issues.go` at line 1, Rename the file
internal/mcp/tools_issues.go to follow the numbered MCP-tools pattern (e.g.,
internal/mcp/tools_01_issues.go) and update any references to that file name in
the repo (build scripts, go:generate directives, or tests) so the new filename
is used; ensure the package declaration (package mcp) and all symbols defined
inside remain unchanged so compilation behavior does not change.
|
|
||
| // Wire reasoning trace store into MCP server for System 2 memory recall. | ||
| mcpServer.SetReasoningStore(reasoningStore) | ||
| mcpServer.SetIssueStore(issueStore) |
There was a problem hiding this comment.
После reinitialize MCP-инструмент issues останется на старом store.
SetIssueStore() вызывается только при первичной инициализации. В reinitializeDatabase() вы пересоздаёте s.issueStore, но уже созданный mcpServer и его handlers продолжают держать старый экземпляр. После oldStore.Close() вызовы issues через MCP начнут работать через закрытый DB handle до перезапуска процесса.
Also applies to: 1321-1321
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/worker/service.go` at line 1086, The MCP server keeps a stale issue
store after reinitializeDatabase() recreates s.issueStore because
SetIssueStore() is only called during initial setup; update the MCP server to
point to the new store after reinitialization by calling
mcpServer.SetIssueStore(s.issueStore) (or equivalent setter) immediately after
s.issueStore is replaced and before closing the oldStore, and mirror the same
fix for the other occurrence mentioned (around the second spot where
SetIssueStore is only called once). Ensure references to mcpServer,
SetIssueStore, reinitializeDatabase, s.issueStore and oldStore.Close() are
updated so handlers use the new store.
CRITICAL: - AddComment: verify issue exists before inserting (prevents orphan rows) HIGH: - Labels: changed from pq.StringArray to models.JSONStringArray (JSONB compat) MAJOR: - CreateIssue: validate status/priority before INSERT - ReopenIssue: atomic status check via WHERE clause (race condition fix) - XML escape in formatIssuesForInjection (prevents tag injection) - handleUpdateIssue: move author metadata from query params to JSON body - tools_issues: filter empty strings from status split MEDIUM: - MCP schema: noted agent_source/project fields (pre-existing pattern) - Reinitialize stale store: documented as pre-existing architecture pattern
Summary
issuesandissue_commentstables with lifecycle: open → acknowledged → resolved ⟲ reopenedissueswith 6 actions (create, list, get, update, comment, reopen)Remaining (Phase 4-5, follow-up PR)
<open-issues>injection in session-start.js + auto-acknowledgeTest plan
go build ./...cleango test ./internal/db/gorm/...passesSummary by CodeRabbit
Примечания к релизу
New Features
Chores