feat: comments marked as decisions, on top of the ticket and searchable

Whoever can change the ticket flags a comment as a decision (new event
CommentDecisionMarkedEvent on the comment). The ticket page lists its
decisions above the description, with a link to the comment in the
conversation (`?comment=<id>` scrolls to it and points at it), and the
search palette finds them by their text across my projects.

API: markDecision / PUT-DELETE /api/comments/{id}/decision; Issue.decisions
and GET /api/issues/{issue}/decisions; decisions(projectId?, search) and
GET /api/projects/{project}/decisions, GET /api/me/decisions. MCP:
mark_decision, search_decisions, and get_issue includes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 11:32:07 +02:00
parent 8f83fdefb6
commit 70e4fbd399
45 changed files with 2515 additions and 84 deletions

View File

@@ -5908,6 +5908,290 @@
]
}
},
"/api/issues/{issue}/decisions": {
"get": {
"operationId": "IssuesController_issueDecisions",
"parameters": [
{
"name": "issue",
"required": true,
"in": "path",
"description": "Ticket id or key",
"schema": {
"example": "WEB-12",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CommentDto"
}
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
},
{}
],
"summary": "Comments of the ticket marked as decisions (oldest first)",
"tags": [
"Issues"
]
}
},
"/api/projects/{project}/decisions": {
"get": {
"operationId": "IssuesController_projectDecisions",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
},
{
"name": "search",
"required": false,
"in": "query",
"description": "Words in the decision",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"maximum": 100,
"default": 20,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DecisionDto"
}
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
},
{}
],
"summary": "Search a project's decisions (most recent first)",
"tags": [
"Issues"
]
}
},
"/api/me/decisions": {
"get": {
"operationId": "IssuesController_myDecisions",
"parameters": [
{
"name": "search",
"required": false,
"in": "query",
"description": "Words in the decision",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"maximum": 100,
"default": 20,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DecisionDto"
}
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Search the decisions of all my projects (most recent first)",
"tags": [
"Issues"
]
}
},
"/api/issues/{issue}/history": {
"get": {
"operationId": "IssuesController_history",
@@ -6632,6 +6916,203 @@
]
}
},
"/api/comments/{commentId}/decision": {
"put": {
"description": "Whoever can change the ticket. Decisions are shown above the ticket and searchable (GET …/decisions).",
"operationId": "CommentsController_markDecision",
"parameters": [
{
"name": "commentId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Mark a comment as a decision of its ticket",
"tags": [
"Comments"
]
},
"delete": {
"operationId": "CommentsController_unmarkDecision",
"parameters": [
{
"name": "commentId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "It is no longer a decision",
"tags": [
"Comments"
]
}
},
"/api/me/releases": {
"get": {
"operationId": "ReleasesController_upcoming",
@@ -15237,6 +15718,25 @@
"$ref": "#/components/schemas/CommentReactionDto"
}
},
"decision": {
"type": "boolean",
"description": "Marked as a decision of the ticket (shown above it)"
},
"decidedBy": {
"nullable": true,
"description": "Who marked it as a decision",
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/UserRefDto"
}
]
},
"decidedAt": {
"type": "string",
"nullable": true,
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
@@ -15258,6 +15758,9 @@
"replyTo",
"deleted",
"reactions",
"decision",
"decidedBy",
"decidedAt",
"createdAt",
"editedAt",
"version"
@@ -15281,6 +15784,21 @@
"total"
]
},
"DecisionDto": {
"type": "object",
"properties": {
"comment": {
"$ref": "#/components/schemas/CommentDto"
},
"issue": {
"$ref": "#/components/schemas/IssueParentDto"
}
},
"required": [
"comment",
"issue"
]
},
"AddCommentDto": {
"type": "object",
"properties": {

View File

@@ -270,6 +270,13 @@ type Comment {
"""Empty on a deleted comment kept for its replies"""
body: String!
createdAt: String!
decidedAt: String
"""Who marked it as a decision"""
decidedBy: UserRef
"""Marked as a decision of the ticket (shown above it)"""
decision: Boolean!
"""Deleted, but kept (without its text) while it has replies"""
deleted: Boolean!
@@ -475,6 +482,12 @@ type DashboardShare {
token: String!
}
"""A comment marked as a decision, with its ticket"""
type Decision {
comment: Comment!
issue: IssueParent!
}
"""A commit, branch or pull request that mentions the ticket"""
type DevLink {
at: String!
@@ -640,6 +653,9 @@ type Issue {
"""Values of the project's custom fields (only those set)"""
customFields: [CustomFieldValue!]!
"""Its comments marked as decisions, oldest first"""
decisions: [Comment!]!
description: String
"""Commits, branches and pull requests that mention it"""
@@ -973,6 +989,11 @@ type Mutation {
"""Relate tickets, e.g. FE DEPENDS_ON BE, QA TESTS FE"""
linkIssues(input: LinkIssuesInput!): CommandResult!
"""
Mark a comment as a decision of its ticket, or unmark it (on: false). Whoever can change the ticket
"""
markDecision(commentId: ID!, on: Boolean! = true): CommandResult!
"""Mark these as read, or all of them when `ids` is omitted"""
markNotificationsRead(ids: [ID!]): Boolean!
@@ -1491,6 +1512,11 @@ type Query {
"""The project's link for clients, if any (owners and admins)"""
dashboardShare(projectId: ID!): DashboardShare
"""
Decisions by their text, most recent first: of a project, or of all my projects without one
"""
decisions(limit: Int! = 20, projectId: ID, search: String): [Decision!]!
"""
A project's epics: top-level tickets that group others, with the progress of those
"""