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

@@ -885,6 +885,57 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/issues/{issue}/decisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Comments of the ticket marked as decisions (oldest first) */
get: operations["IssuesController_issueDecisions"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/projects/{project}/decisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Search a project's decisions (most recent first) */
get: operations["IssuesController_projectDecisions"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/me/decisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Search the decisions of all my projects (most recent first) */
get: operations["IssuesController_myDecisions"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/issues/{issue}/history": {
parameters: {
query?: never;
@@ -978,6 +1029,27 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/comments/{commentId}/decision": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* Mark a comment as a decision of its ticket
* @description Whoever can change the ticket. Decisions are shown above the ticket and searchable (GET …/decisions).
*/
put: operations["CommentsController_markDecision"];
post?: never;
/** It is no longer a decision */
delete: operations["CommentsController_unmarkDecision"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/me/releases": {
parameters: {
query?: never;
@@ -2929,6 +3001,12 @@ export interface components {
deleted: boolean;
/** @description In the order they are offered; only the ones somebody chose */
reactions: components["schemas"]["CommentReactionDto"][];
/** @description Marked as a decision of the ticket (shown above it) */
decision: boolean;
/** @description Who marked it as a decision */
decidedBy: components["schemas"]["UserRefDto"] | null;
/** Format: date-time */
decidedAt: string | null;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
@@ -2939,6 +3017,10 @@ export interface components {
items: components["schemas"]["CommentDto"][];
total: number;
};
DecisionDto: {
comment: components["schemas"]["CommentDto"];
issue: components["schemas"]["IssueParentDto"];
};
AddCommentDto: {
/** @example Reproducido en Safari 17. */
body: string;
@@ -7578,6 +7660,185 @@ export interface operations {
};
};
};
IssuesController_issueDecisions: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Ticket id or key */
issue: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommentDto"][];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_projectDecisions: {
parameters: {
query?: {
/** @description Words in the decision */
search?: string;
limit?: number;
};
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DecisionDto"][];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_myDecisions: {
parameters: {
query?: {
/** @description Words in the decision */
search?: string;
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DecisionDto"][];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
IssuesController_history: {
parameters: {
query?: {
@@ -8087,6 +8348,156 @@ export interface operations {
};
};
};
CommentsController_markDecision: {
parameters: {
query?: never;
header?: never;
path: {
commentId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
CommentsController_unmarkDecision: {
parameters: {
query?: never;
header?: never;
path: {
commentId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CommandResultDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
ReleasesController_upcoming: {
parameters: {
query?: {