diff --git a/apps/api/openapi.json b/apps/api/openapi.json
index a8571f7..0840d32 100644
--- a/apps/api/openapi.json
+++ b/apps/api/openapi.json
@@ -3683,6 +3683,108 @@
]
}
},
+ "/api/issues/{issue}/visit": {
+ "put": {
+ "description": "Answers when I had opened it before (null: never), to show what others did since. Private to me.",
+ "operationId": "IssuesController_visit",
+ "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": {
+ "$ref": "#/components/schemas/IssueVisitDto"
+ }
+ }
+ }
+ },
+ "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": "I opened this ticket now",
+ "tags": [
+ "Issues"
+ ]
+ }
+ },
"/api/issues/{issue}/follow-tree": {
"put": {
"description": "Told of its changes and of every ticket hanging from it, at any depth, also the ones added later. Private to me.",
@@ -8436,6 +8538,116 @@
]
}
},
+ "/api/comments/{commentId}/issue": {
+ "post": {
+ "description": "Its text becomes the description, as a subtask of its ticket or linked to it (RELATES_TO); the comment shows where it went. Answers the new ticket. Whoever can change tickets.",
+ "operationId": "CommentsController_turnIntoIssue",
+ "parameters": [
+ {
+ "name": "commentId",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TurnCommentIntoIssueDto"
+ }
+ }
+ }
+ },
+ "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": "Make a ticket of a comment",
+ "tags": [
+ "Comments"
+ ]
+ }
+ },
"/api/comments/{commentId}/resolution": {
"put": {
"description": "Whoever can change the ticket. A conclusion is added as a reply marked as a decision. The web folds a settled conversation.",
@@ -17708,6 +17920,20 @@
"verdict"
]
},
+ "IssueVisitDto": {
+ "type": "object",
+ "properties": {
+ "previousVisit": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true,
+ "description": "My visit before this one (null: first time)"
+ }
+ },
+ "required": [
+ "previousVisit"
+ ]
+ },
"IssuePageDto": {
"type": "object",
"properties": {
@@ -18190,6 +18416,22 @@
"winner"
]
},
+ "CommentSpawnDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "example": "WEB-45"
+ }
+ },
+ "required": [
+ "id",
+ "key"
+ ]
+ },
"CommentDto": {
"type": "object",
"properties": {
@@ -18272,6 +18514,13 @@
"nullable": true,
"format": "date-time"
},
+ "spawned": {
+ "description": "Tickets made from it («Convertir en ticket»), oldest first",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/CommentSpawnDto"
+ }
+ },
"createdAt": {
"type": "string",
"format": "date-time"
@@ -18300,6 +18549,7 @@
"resolved",
"resolvedBy",
"resolvedAt",
+ "spawned",
"createdAt",
"editedAt",
"version"
@@ -18476,6 +18726,29 @@
}
}
},
+ "TurnCommentIntoIssueDto": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "example": "También falla en Android",
+ "maxLength": 200
+ },
+ "type": {
+ "type": "string",
+ "description": "One of the project's ticket type ids"
+ },
+ "asSubtask": {
+ "type": "boolean",
+ "default": false,
+ "description": "Hang it from the ticket of the comment; otherwise it is linked to it (RELATES_TO)"
+ }
+ },
+ "required": [
+ "title",
+ "type"
+ ]
+ },
"ResolveThreadDto": {
"type": "object",
"properties": {
diff --git a/apps/api/schema.gql b/apps/api/schema.gql
index 73102c0..cb5a717 100644
--- a/apps/api/schema.gql
+++ b/apps/api/schema.gql
@@ -318,6 +318,9 @@ type Comment {
"""Who settled it"""
resolvedBy: UserRef
+
+ """Tickets made from it («Convertir en ticket»), oldest first"""
+ spawned: [CommentSpawn!]!
version: Int!
}
@@ -354,6 +357,12 @@ type CommentReaction {
users: [UserRef!]!
}
+"""A ticket made from a comment"""
+type CommentSpawn {
+ id: ID!
+ key: String!
+}
+
type CountByAssignee {
count: Int!
@@ -1209,6 +1218,18 @@ type Mutation {
"""
trashIssue(issueId: ID!): CommandResult!
+ """
+ Make a ticket of a comment: its text becomes the description, as a subtask of its ticket or linked to it (RELATES_TO). Answers the new ticket. Whoever can change tickets
+ """
+ turnCommentIntoIssue(
+ asSubtask: Boolean! = false
+ commentId: ID!
+ title: String!
+
+ """One of the project's ticket type ids"""
+ type: String!
+ ): CommandResult!
+
"""
I am writing on this ticket (send every few seconds while typing): those with it open see it at once via `typing`
"""
@@ -1258,6 +1279,11 @@ type Mutation {
updateSprint(input: UpdateSprintInput!): CommandResult!
updateWebhook(input: UpdateWebhookInput!): Webhook!
+ """
+ I opened this ticket now: answers when I had opened it before (null: never), to show what is new since
+ """
+ visitIssue(issueId: ID!): String
+
"""
Vote for a ticket (on) or take the vote back: anyone signed in on a public project, members on a private one
"""
diff --git a/apps/mcp/src/app/api/taskstream.schema.ts b/apps/mcp/src/app/api/taskstream.schema.ts
index 38a95d2..e8a7ccb 100644
--- a/apps/mcp/src/app/api/taskstream.schema.ts
+++ b/apps/mcp/src/app/api/taskstream.schema.ts
@@ -632,6 +632,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/issues/{issue}/visit": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /**
+ * I opened this ticket now
+ * @description Answers when I had opened it before (null: never), to show what others did since. Private to me.
+ */
+ put: operations["IssuesController_visit"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/issues/{issue}/follow-tree": {
parameters: {
query?: never;
@@ -1241,6 +1261,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/comments/{commentId}/issue": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Make a ticket of a comment
+ * @description Its text becomes the description, as a subtask of its ticket or linked to it (RELATES_TO); the comment shows where it went. Answers the new ticket. Whoever can change tickets.
+ */
+ post: operations["CommentsController_turnIntoIssue"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/comments/{commentId}/resolution": {
parameters: {
query?: never;
@@ -3181,6 +3221,13 @@ export interface components {
verdict: components["schemas"]["ReviewVerdict"];
answer?: string;
};
+ IssueVisitDto: {
+ /**
+ * Format: date-time
+ * @description My visit before this one (null: first time)
+ */
+ previousVisit: string | null;
+ };
IssuePageDto: {
items: components["schemas"]["IssueDto"][];
/** @description Total matches (for pagination) */
@@ -3377,6 +3424,11 @@ export interface components {
/** @description Index of the winning option once closed (null on a tie) */
winner: number | null;
};
+ CommentSpawnDto: {
+ id: string;
+ /** @example WEB-45 */
+ key: string;
+ };
CommentDto: {
/** Format: uuid */
id: string;
@@ -3407,6 +3459,8 @@ export interface components {
resolvedBy: components["schemas"]["UserRefDto"] | null;
/** Format: date-time */
resolvedAt: string | null;
+ /** @description Tickets made from it («Convertir en ticket»), oldest first */
+ spawned: components["schemas"]["CommentSpawnDto"][];
/** Format: date-time */
createdAt: string;
/** Format: date-time */
@@ -3490,6 +3544,17 @@ export interface components {
*/
decide: boolean;
};
+ TurnCommentIntoIssueDto: {
+ /** @example También falla en Android */
+ title: string;
+ /** @description One of the project's ticket type ids */
+ type: string;
+ /**
+ * @description Hang it from the ticket of the comment; otherwise it is linked to it (RELATES_TO)
+ * @default false
+ */
+ asSubtask: boolean;
+ };
ResolveThreadDto: {
/** @description What was settled: added as a reply marked as a decision before it closes */
conclusion?: string;
@@ -6732,6 +6797,82 @@ export interface operations {
};
};
};
+ IssuesController_visit: {
+ 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"]["IssueVisitDto"];
+ };
+ };
+ /** @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"];
+ };
+ };
+ };
+ };
IssuesController_followTree: {
parameters: {
query?: never;
@@ -9935,6 +10076,85 @@ export interface operations {
};
};
};
+ CommentsController_turnIntoIssue: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ commentId: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TurnCommentIntoIssueDto"];
+ };
+ };
+ 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_resolveThread: {
parameters: {
query?: never;
diff --git a/apps/mcp/src/app/tools/issues.resolver.ts b/apps/mcp/src/app/tools/issues.resolver.ts
index 7994056..7ffdc64 100644
--- a/apps/mcp/src/app/tools/issues.resolver.ts
+++ b/apps/mcp/src/app/tools/issues.resolver.ts
@@ -599,6 +599,37 @@ export class IssuesResolver {
);
}
+ @Tool({
+ name: 'turn_comment_into_issue',
+ description:
+ "Make a ticket of a comment that turned out to be work of its own: its text becomes the description, as a subtask of the comment's ticket (asSubtask) or linked to it. Read type ids with get_project. Answers the new ticket's key.",
+ paramsSchema: z.object({
+ commentId: z.string().min(1),
+ title: z.string().min(1).max(200),
+ type: z.string().min(1).describe("One of the project's ticket type ids"),
+ asSubtask: z.boolean().default(false),
+ }),
+ annotations: WRITE,
+ })
+ turnCommentIntoIssue(
+ {
+ commentId,
+ title,
+ type,
+ asSubtask,
+ }: { commentId: string; title: string; type: string; asSubtask: boolean },
+ ctx: McpContext,
+ ) {
+ return toolResult(() =>
+ this.api.data(
+ this.api.as(ctx).POST('/api/comments/{commentId}/issue', {
+ params: { path: { commentId } },
+ body: { title, type, asSubtask },
+ }),
+ ),
+ );
+ }
+
@Tool({
name: 'link_issues',
description:
diff --git a/docs/01-vision-general.md b/docs/01-vision-general.md
index 86c7639..18051b3 100644
--- a/docs/01-vision-general.md
+++ b/docs/01-vision-general.md
@@ -2,51 +2,51 @@
## Qué resuelve
-| Necesidad | Cómo se resuelve |
-| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 🐛 Bugs | Tickets de tipo `BUG` con prioridad, área y etiquetas |
-| 🌍 Públicos | Un proyecto público se puede leer sin iniciar sesión (web, GraphQL y REST): tablero, tickets, comentarios, releases, actividad y estadísticas; «Explorar» los lista |
-| 🗑️ Borrar | El propietario borra un proyecto confirmando su clave; tickets, comentarios, releases y enlaces desde otros proyectos se borran en cascada (sagas) y la clave queda libre |
-| 🧩 Configuración | Cada proyecto tiene sus estados (con categoría, color y motivo obligatorio), transiciones, tipos de ticket y áreas; lo usado se archiva, no se borra |
-| 🎨 Cambios de UI | Tipo `UI_CHANGE` con enlace al frame de Figma (validado contra `figma.com`) |
-| ⚙️ Features | Tipo `FEATURE` siguiendo el workflow completo |
-| 🧪 QA | Por defecto, `READY_FOR_QA → IN_QA → DONE / QA_FAILED`; fallar QA exige un motivo |
-| 🚀 Releases | Versiones a las que se asignan tickets; no se publican con tickets sin terminar (salvo forzando) |
-| 📝 Notas de versión | Borrador en Markdown con los tickets terminados por tipo, editable y copiable, también tras publicar; y una página pública por release para los clientes, con enlace secreto |
-| 👥 Asignaciones | Solo a miembros del proyecto; panel de carga por persona |
-| 💬 Comentarios | Decisiones y explicaciones dentro del ticket, con formato (editor visual, guardado en Markdown), capturas pegadas, respuestas, reacciones (👍 🎉…), decisiones (un comentario marcado sale encima del ticket y se encuentra al buscar), conversaciones que se resuelven con su conclusión, encuestas rápidas que acaban en decisión, «está escribiendo…» en vivo y `@menciones`; el autor edita, el admin modera |
-| 🔎 Vistas | El tablero se filtra por texto, persona (o sin asignar), tipo, prioridad, área y release; los filtros se guardan como vistas propias o compartidas con el proyecto |
-| 📋 Lista | Todos los tickets en filas: ordenar por cualquier columna, filtrar, cambiar estado, prioridad o persona sin abrirlos, editar en bloque y duplicar |
-| ↩️ Deshacer | Mover, reasignar o mandar a la papelera ofrece «Deshacer» unos segundos (también Ctrl/⌘ Z), con la orden contraria |
-| ☀ Mi día | Cada mañana eliges los tickets de hoy; se tachan al terminar y lo que quede se pasa al día siguiente; y «Recordármelo» en cualquier ticket |
-| 🚦 Salud | Un semáforo por proyecto (en su panel y en la cartera) que dice qué va mal y por qué: atascados, vencidos, bugs urgentes, QA, releases tarde |
-| 🚦 WIP y carriles | Límite de trabajo en curso por estado (la columna se marca al pasarse) y tablero agrupable por persona, épica, prioridad o tipo |
-| 🎲 Estimación | Puntos por ticket (escala 0-1-2-3-5-8-13-21) en la ficha, las tarjetas, las columnas y el backlog; velocidad por sprint y aviso si un sprint planificado supera la media |
-| 🏃 Sprints | Iteraciones con fechas y objetivo, una en curso por proyecto, con burndown (tickets o puntos) y traspaso de lo pendiente al cerrar |
-| 📅 Calendario | Los sprints mes a mes como franjas, con las releases y los vencimientos de cada día; se planifica un sprint pulsando su primer día y todo se descarga en .ics |
-| 📋 Backlog | Lo pendiente de empezar, en el orden en que se va a coger; se reordena arrastrando y el tablero sigue ese orden |
-| 🗑️ Papelera | Borrar un ticket lo manda a la papelera (quien lo abrió o un admin); se recupera durante 30 días y luego se borra del todo |
-| ☑️ En bloque | Selecciona varios tickets del backlog y cámbiales a la vez estado, persona, sprint, prioridad o etiqueta |
-| ⌨️ Atajos y búsqueda | Ctrl/⌘ K o / busca tickets, decisiones y proyectos de todos tus proyectos (antes de escribir, tus tickets fijados); `g b`, `g k`, `g s`, `g r` navegan, `c` crea, en el tablero las flechas recorren las tarjetas y Shift+←/→ las cambia de columna, `e` edita, `?` los lista |
-| 🗺️ Hoja de ruta | Las épicas en el tiempo (fecha de inicio y límite), con los sprints y las releases como referencias y flechas entre las que dependen unas de otras |
-| 🗳️ Propuestas y votos | Un proyecto público puede abrir un formulario para que cualquiera, sin cuenta, reporte un error o proponga una idea; quien tenga sesión vota lo que quiere |
-| 🌐 Web del colectivo | La página de cada proyecto en pireon.dev enseña su hoja de ruta, versiones, sprint y tickets para recién llegados (`good-first-issue`) |
-| 🧩 Desglose | Épicas → tickets → subtareas (tres niveles), con su progreso; y una checklist dentro de cada ticket para los pasos pequeños |
-| 👀 Seguir | Cualquier miembro sigue los tickets que le interesan y los ve juntos en «Que sigo»; quien lo abre, lo tiene asignado o comenta, lo sigue solo |
-| 🔔 Avisos | Campana con lo que te concierne (asignaciones, cambios de estado, QA fallido, comentarios, respuestas), en vivo y opcionalmente por correo |
-| ⏰ Recordatorios | Aviso (y correo, si se quiere) el día antes, el mismo día y al vencer la fecha límite, a quien lo tiene y a quien lo sigue; las tarjetas marcan lo que vence |
-| 📎 Adjuntos | Capturas, logs y vídeos en el ticket o bajo un comentario; arrastrar, elegir o pegar con Ctrl+V; miniaturas de las imágenes; grabar la pantalla o la voz desde el comentario y verlo ahí mismo |
-| 🔗 Relaciones | `BLOCKS`, `DEPENDS_ON`, `TESTS`, `DUPLICATES`, `RELATES_TO`, también entre proyectos (FE ↔ BE ↔ QA); lo que espera a otro ticket abierto sale marcado como «Bloqueado» |
-| 🌿 Git | Webhook para GitHub, Gitea o GitLab: commits, ramas y pull requests que nombran un ticket (WEB-12) aparecen en su ficha |
-| 🚨 Alertas → bugs | Sentry, Uptime Kuma, Grafana o cualquier herramienta abren bugs por una URL secreta; el mismo problema repetido va al bug abierto, y «resuelta» se dice en él |
-| 🧾 Plantillas | Cada tipo de ticket puede traer una descripción inicial y una checklist que se aplican al crearlo |
-| 🤖 Automatizaciones | Reglas «cuando… si… entonces…»: al crear, mover, asignar, comentar o fusionar una PR, mover, asignar, etiquetar, priorizar, comentar o meter en el sprint |
-| 📡 Webhooks | Un POST firmado (HMAC-SHA256) a otro sistema cuando pasa algo: tickets, comentarios, releases, sprints; con prueba y último envío |
-| 📦 Importar/exportar | Todos los tickets a CSV; importar un CSV propio o una exportación de Jira, con vista previa de lo que se va a crear |
-| 🏠 Inicio | «Mi trabajo»: mis tickets en todos los proyectos, por categoría (QA fallidos primero), lo que espera QA, próximas releases e «Ir a…» por clave |
-| 📊 Seguimiento | Panel con contadores, flujo por estado, el equipo con sus tickets abiertos (enlaza a su tablero), feed de actividad e historial por ticket |
-| 📈 Estadísticas | Creados vs. resueltos, flujo acumulado, burnup de releases, tiempos de ciclo y entrega, tasa de fallo en QA y resueltos por persona |
-| ⚡ Tiempo real | Tablero, comentarios, actividad, releases y membresías se actualizan solos (GraphQL subscriptions) |
+| Necesidad | Cómo se resuelve |
+| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 🐛 Bugs | Tickets de tipo `BUG` con prioridad, área y etiquetas |
+| 🌍 Públicos | Un proyecto público se puede leer sin iniciar sesión (web, GraphQL y REST): tablero, tickets, comentarios, releases, actividad y estadísticas; «Explorar» los lista |
+| 🗑️ Borrar | El propietario borra un proyecto confirmando su clave; tickets, comentarios, releases y enlaces desde otros proyectos se borran en cascada (sagas) y la clave queda libre |
+| 🧩 Configuración | Cada proyecto tiene sus estados (con categoría, color y motivo obligatorio), transiciones, tipos de ticket y áreas; lo usado se archiva, no se borra |
+| 🎨 Cambios de UI | Tipo `UI_CHANGE` con enlace al frame de Figma (validado contra `figma.com`) |
+| ⚙️ Features | Tipo `FEATURE` siguiendo el workflow completo |
+| 🧪 QA | Por defecto, `READY_FOR_QA → IN_QA → DONE / QA_FAILED`; fallar QA exige un motivo |
+| 🚀 Releases | Versiones a las que se asignan tickets; no se publican con tickets sin terminar (salvo forzando) |
+| 📝 Notas de versión | Borrador en Markdown con los tickets terminados por tipo, editable y copiable, también tras publicar; y una página pública por release para los clientes, con enlace secreto |
+| 👥 Asignaciones | Solo a miembros del proyecto; panel de carga por persona |
+| 💬 Comentarios | Decisiones y explicaciones dentro del ticket, con formato (editor visual, guardado en Markdown), capturas pegadas, respuestas, reacciones (👍 🎉…), decisiones (un comentario marcado sale encima del ticket y se encuentra al buscar), conversaciones que se resuelven con su conclusión, convertir un comentario en ticket, lo nuevo desde tu última visita marcado, encuestas rápidas que acaban en decisión, «está escribiendo…» en vivo y `@menciones`; el autor edita, el admin modera |
+| 🔎 Vistas | El tablero se filtra por texto, persona (o sin asignar), tipo, prioridad, área y release; los filtros se guardan como vistas propias o compartidas con el proyecto |
+| 📋 Lista | Todos los tickets en filas: ordenar por cualquier columna, filtrar, cambiar estado, prioridad o persona sin abrirlos, editar en bloque y duplicar |
+| ↩️ Deshacer | Mover, reasignar o mandar a la papelera ofrece «Deshacer» unos segundos (también Ctrl/⌘ Z), con la orden contraria |
+| ☀ Mi día | Cada mañana eliges los tickets de hoy; se tachan al terminar y lo que quede se pasa al día siguiente; y «Recordármelo» en cualquier ticket |
+| 🚦 Salud | Un semáforo por proyecto (en su panel y en la cartera) que dice qué va mal y por qué: atascados, vencidos, bugs urgentes, QA, releases tarde |
+| 🚦 WIP y carriles | Límite de trabajo en curso por estado (la columna se marca al pasarse) y tablero agrupable por persona, épica, prioridad o tipo |
+| 🎲 Estimación | Puntos por ticket (escala 0-1-2-3-5-8-13-21) en la ficha, las tarjetas, las columnas y el backlog; velocidad por sprint y aviso si un sprint planificado supera la media |
+| 🏃 Sprints | Iteraciones con fechas y objetivo, una en curso por proyecto, con burndown (tickets o puntos) y traspaso de lo pendiente al cerrar |
+| 📅 Calendario | Los sprints mes a mes como franjas, con las releases y los vencimientos de cada día; se planifica un sprint pulsando su primer día y todo se descarga en .ics |
+| 📋 Backlog | Lo pendiente de empezar, en el orden en que se va a coger; se reordena arrastrando y el tablero sigue ese orden |
+| 🗑️ Papelera | Borrar un ticket lo manda a la papelera (quien lo abrió o un admin); se recupera durante 30 días y luego se borra del todo |
+| ☑️ En bloque | Selecciona varios tickets del backlog y cámbiales a la vez estado, persona, sprint, prioridad o etiqueta |
+| ⌨️ Atajos y búsqueda | Ctrl/⌘ K o / busca tickets, decisiones y proyectos de todos tus proyectos (antes de escribir, tus tickets fijados); `g b`, `g k`, `g s`, `g r` navegan, `c` crea, en el tablero las flechas recorren las tarjetas y Shift+←/→ las cambia de columna, `e` edita, `?` los lista |
+| 🗺️ Hoja de ruta | Las épicas en el tiempo (fecha de inicio y límite), con los sprints y las releases como referencias y flechas entre las que dependen unas de otras |
+| 🗳️ Propuestas y votos | Un proyecto público puede abrir un formulario para que cualquiera, sin cuenta, reporte un error o proponga una idea; quien tenga sesión vota lo que quiere |
+| 🌐 Web del colectivo | La página de cada proyecto en pireon.dev enseña su hoja de ruta, versiones, sprint y tickets para recién llegados (`good-first-issue`) |
+| 🧩 Desglose | Épicas → tickets → subtareas (tres niveles), con su progreso; y una checklist dentro de cada ticket para los pasos pequeños |
+| 👀 Seguir | Cualquier miembro sigue los tickets que le interesan y los ve juntos en «Que sigo»; quien lo abre, lo tiene asignado o comenta, lo sigue solo |
+| 🔔 Avisos | Campana con lo que te concierne (asignaciones, cambios de estado, QA fallido, comentarios, respuestas), en vivo y opcionalmente por correo |
+| ⏰ Recordatorios | Aviso (y correo, si se quiere) el día antes, el mismo día y al vencer la fecha límite, a quien lo tiene y a quien lo sigue; las tarjetas marcan lo que vence |
+| 📎 Adjuntos | Capturas, logs y vídeos en el ticket o bajo un comentario; arrastrar, elegir o pegar con Ctrl+V; miniaturas de las imágenes; grabar la pantalla o la voz desde el comentario y verlo ahí mismo |
+| 🔗 Relaciones | `BLOCKS`, `DEPENDS_ON`, `TESTS`, `DUPLICATES`, `RELATES_TO`, también entre proyectos (FE ↔ BE ↔ QA); lo que espera a otro ticket abierto sale marcado como «Bloqueado» |
+| 🌿 Git | Webhook para GitHub, Gitea o GitLab: commits, ramas y pull requests que nombran un ticket (WEB-12) aparecen en su ficha |
+| 🚨 Alertas → bugs | Sentry, Uptime Kuma, Grafana o cualquier herramienta abren bugs por una URL secreta; el mismo problema repetido va al bug abierto, y «resuelta» se dice en él |
+| 🧾 Plantillas | Cada tipo de ticket puede traer una descripción inicial y una checklist que se aplican al crearlo |
+| 🤖 Automatizaciones | Reglas «cuando… si… entonces…»: al crear, mover, asignar, comentar o fusionar una PR, mover, asignar, etiquetar, priorizar, comentar o meter en el sprint |
+| 📡 Webhooks | Un POST firmado (HMAC-SHA256) a otro sistema cuando pasa algo: tickets, comentarios, releases, sprints; con prueba y último envío |
+| 📦 Importar/exportar | Todos los tickets a CSV; importar un CSV propio o una exportación de Jira, con vista previa de lo que se va a crear |
+| 🏠 Inicio | «Mi trabajo»: mis tickets en todos los proyectos, por categoría (QA fallidos primero), lo que espera QA, próximas releases e «Ir a…» por clave |
+| 📊 Seguimiento | Panel con contadores, flujo por estado, el equipo con sus tickets abiertos (enlaza a su tablero), feed de actividad e historial por ticket |
+| 📈 Estadísticas | Creados vs. resueltos, flujo acumulado, burnup de releases, tiempos de ciclo y entrega, tasa de fallo en QA y resueltos por persona |
+| ⚡ Tiempo real | Tablero, comentarios, actividad, releases y membresías se actualizan solos (GraphQL subscriptions) |
Los **usuarios no se gestionan aquí**: vienen del servidor de identidad **Pireon**
([Swagger](https://oauth.pireon.dev/api/documentation)). TaskStream gestiona proyectos, membresías y roles por
diff --git a/docs/03-api.md b/docs/03-api.md
index b6aff6f..dfdf3e7 100644
--- a/docs/03-api.md
+++ b/docs/03-api.md
@@ -245,6 +245,11 @@ stateDiagram-v2
admin, la cierra (`closePoll(commentId, decide)`, `POST …/poll/close`; `CommentPollClosedEvent`): gana la más votada
(con empate, ninguna) y, con `decide`, se añade una respuesta «Decidido: …» marcada como decisión. El comentario da
`poll { options { text voters } closed winner }`.
+- **Convertir en ticket**: un comentario que resulta ser trabajo propio se convierte en ticket
+ (`turnCommentIntoIssue(commentId, title, type, asSubtask)`, `POST /api/comments/{id}/issue`), como subtarea del ticket
+ del comentario o enlazado a él (`RELATES_TO`), por quien puede cambiar tickets. El texto del comentario es la
+ descripción (sin la plantilla del tipo), con una línea de dónde sale; el comentario lo recuerda
+ (`CommentTurnedIntoIssueEvent`) y da `spawned { id key }`. Devuelve el ticket nuevo.
- **Conversaciones**: un comentario raíz y sus respuestas se resuelven (`resolveThread(commentId, resolved, conclusion)`,
`PUT/DELETE /api/comments/{id}/resolution`; evento `CommentThreadResolvedEvent`) por quien puede cambiar el ticket;
una respuesta no se resuelve sola. Si trae `conclusion`, antes de cerrarla se añade como respuesta marcada como
@@ -454,7 +459,10 @@ historial (`my_days`). **Recordármelo**: cada persona pone en un ticket un reco
/api/issues/{issue}/reminder`, `GET /api/me/reminders`): uno por ticket, en el futuro y como mucho a un año. Cada minuto
`IssueReminderScheduler` avisa de los que tocan (`REMINDER`, por la campana, el navegador y el correo; una vez, por su
id) y olvida los de tickets en la papelera o fuera de alcance. Privados y sin historial (`issue_reminders`);
-`REMINDERS_ENABLED=false` los apaga junto con los de fecha límite. **Seguir todo**: seguir un ticket con todo lo que cuelga de él (una épica, un ticket con subtareas):
+`REMINDERS_ENABLED=false` los apaga junto con los de fecha límite. **Lo nuevo desde tu última visita**: al abrir un
+ticket, `visitIssue(issueId)` (`PUT /api/issues/{issue}/visit`) apunta la visita y devuelve la anterior (null la
+primera vez); con ella el cliente marca lo que otros han hecho desde entonces. Personal y sin historial (colección
+`issue_visits`). **Seguir todo**: seguir un ticket con todo lo que cuelga de él (una épica, un ticket con subtareas):
`followTree`/`unfollowTree`, `Issue.followingTree`; `PUT/DELETE /api/issues/{issue}/follow-tree`. Quien lo hace oye lo
mismo que un seguidor de cada ticket de debajo, a cualquier profundidad, también de los que se añadan, y además cuándo
uno entra (`IssueParentChangedEvent`). Personal y sin historial (colección `tree_follows`); los invitados de un proyecto
@@ -573,17 +581,17 @@ que funcionan también sin token.
Todas devuelven `CommandResult { id, key, version }`.
-| Ámbito | Mutations |
-| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| Proyectos | `createProject`, `updateProject`, `updateProjectSettings`, `deleteProject(confirmKey)`, `addProjectMember`, `changeProjectMemberRole`, `removeProjectMember` |
-| Tickets | `createIssue`, `updateIssue`, `changeIssueStatus`, `assignIssue`, `linkIssues`, `unlinkIssues`, `setIssueRelease`, `setIssueParent`, `rankIssue`, `addChecklistItem`, `updateChecklistItem`, `removeChecklistItem`, `watchIssue`, `unwatchIssue`, `requestReview`, `answerReview`, `cancelReview`, `pinIssue`, `unpinIssue`, `followTree`, `unfollowTree`, `clearReminder`, `addToMyDay`, `removeFromMyDay` (devuelven `true`), `setReminder`, `removeAttachment` (subir: por REST), `duplicateIssue`, `voteIssue` |
-| Comentarios | `addComment(replyTo, poll)`, `votePoll`, `closePoll`, `editComment`, `deleteComment`, `reactToComment`, `markDecision(commentId, on)`, `resolveThread(commentId, resolved, conclusion)`, `typing(issueId, threadId)`; consulta `decisions(projectId, search)` |
-| Webhooks | `createWebhook` (devuelve el secreto), `updateWebhook`, `deleteWebhook`, `testWebhook` |
-| Git | `addGitRepository` (devuelve su secreto), `rotateGitRepositorySecret`, `removeGitRepository` |
-| Sprints | `createSprint`, `updateSprint`, `startSprint`, `closeSprint(carryOverToSprintId)`, `deleteSprint`, `setIssueSprint` |
-| Vistas | `saveFilter(input)` (devuelve el id), `deleteSavedFilter(id)`, `GET/POST /api/me/views`, `DELETE /api/me/views/{id}` (mis vistas de «Mi trabajo») |
-| Avisos | `markNotificationsRead(ids)`, `updateNotificationSettings(email)` |
-| Releases | `createRelease`, `updateRelease`, `publishRelease(force)`, `writeReleaseNotes`, `archiveRelease` |
+| Ámbito | Mutations |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Proyectos | `createProject`, `updateProject`, `updateProjectSettings`, `deleteProject(confirmKey)`, `addProjectMember`, `changeProjectMemberRole`, `removeProjectMember` |
+| Tickets | `createIssue`, `updateIssue`, `changeIssueStatus`, `assignIssue`, `linkIssues`, `unlinkIssues`, `setIssueRelease`, `setIssueParent`, `rankIssue`, `addChecklistItem`, `updateChecklistItem`, `removeChecklistItem`, `watchIssue`, `unwatchIssue`, `requestReview`, `answerReview`, `cancelReview`, `pinIssue`, `unpinIssue`, `followTree`, `unfollowTree`, `visitIssue`, `clearReminder`, `addToMyDay`, `removeFromMyDay` (devuelven `true`), `setReminder`, `removeAttachment` (subir: por REST), `duplicateIssue`, `voteIssue` |
+| Comentarios | `addComment(replyTo, poll)`, `votePoll`, `closePoll`, `editComment`, `deleteComment`, `reactToComment`, `markDecision(commentId, on)`, `resolveThread(commentId, resolved, conclusion)`, `turnCommentIntoIssue(commentId, title, type, asSubtask)`, `typing(issueId, threadId)`; consulta `decisions(projectId, search)` |
+| Webhooks | `createWebhook` (devuelve el secreto), `updateWebhook`, `deleteWebhook`, `testWebhook` |
+| Git | `addGitRepository` (devuelve su secreto), `rotateGitRepositorySecret`, `removeGitRepository` |
+| Sprints | `createSprint`, `updateSprint`, `startSprint`, `closeSprint(carryOverToSprintId)`, `deleteSprint`, `setIssueSprint` |
+| Vistas | `saveFilter(input)` (devuelve el id), `deleteSavedFilter(id)`, `GET/POST /api/me/views`, `DELETE /api/me/views/{id}` (mis vistas de «Mi trabajo») |
+| Avisos | `markNotificationsRead(ids)`, `updateNotificationSettings(email)` |
+| Releases | `createRelease`, `updateRelease`, `publishRelease(force)`, `writeReleaseNotes`, `archiveRelease` |
### Subscriptions
@@ -786,7 +794,7 @@ Proyectos y tickets se pueden indicar por **id o por clave** (`WEB`, `WEB-12`).
| Usuarios | `GET /api/users/me`, `GET /api/users?search=`, `GET /api/users/{id}` |
| Proyectos | `GET/POST /api/projects`, `GET/PATCH/DELETE /api/projects/{project}` (`DELETE …?confirm=CLAVE`), `POST …/archive`, `POST …/unarchive`, `PUT …/settings`, `…/board`, `…/epics`, `…/stats`, `…/analytics?days=`, `…/insights`, `…/activity`, `…/members` (POST, PATCH, DELETE) |
| Tickets | `GET /api/me/home` (portada), `GET /api/me/portfolio` (cartera), `GET /api/me/issues` (mi trabajo; filtros `project`, `priority`, `bugsOnly`, `overdue`), `GET/POST /api/projects/{project}/issues`, `GET/PATCH /api/issues/{issue}`, `PUT …/status`, `PUT …/assignee`, `PUT …/release`, `PUT/DELETE …/watch`, `POST …/reviews`, `POST …/reviews/{id}/answer`, `DELETE …/reviews/{id}`, `PUT/DELETE …/follow-tree`, `PUT/DELETE …/reminder`, `GET /api/me/reminders`, `GET /api/me/day`, `PUT/DELETE /api/me/day/{issue}`, `GET /api/me/pins` y `PUT/DELETE …/pin` (fijados), `PUT/DELETE …/presence`, `PUT …/parent`, `PUT …/rank`, `POST …/checklist`, `PATCH/DELETE …/checklist/{item}`, `POST/DELETE …/links`, `GET/POST …/comments`, `GET …/history`, `POST /api/issues/{issue}/duplicate`, `PUT/DELETE …/vote` |
-| Comentarios | `PATCH/DELETE /api/comments/{commentId}`, `POST …/reactions`, `PUT/DELETE …/decision`, `PUT/DELETE …/resolution`, `POST …/poll/vote`, `POST …/poll/close`; `GET /api/issues/{issue}/decisions`, `GET /api/projects/{project}/decisions`, `GET /api/me/decisions` |
+| Comentarios | `PATCH/DELETE /api/comments/{commentId}`, `POST …/reactions`, `PUT/DELETE …/decision`, `PUT/DELETE …/resolution`, `POST …/poll/vote`, `POST …/poll/close`, `POST …/issue` (convertir en ticket); `GET /api/issues/{issue}/decisions`, `GET /api/projects/{project}/decisions`, `GET /api/me/decisions` |
| Sprints | `GET/POST /api/projects/{project}/sprints`, `GET/PATCH/DELETE /api/sprints/{sprint}`, `POST …/start`, `POST …/close`, `GET …/progress`, `…/burndown`, `…/issues`, `PUT /api/issues/{issue}/sprint` |
| Vistas | `GET/POST /api/projects/{project}/filters`, `DELETE /api/filters/{id}` |
| Avisos | `GET /api/me/notifications`, `POST /api/me/notifications/read`, `GET/PUT /api/me/notification-settings` |
diff --git a/docs/04-cliente-web.md b/docs/04-cliente-web.md
index ad3431d..192a402 100644
--- a/docs/04-cliente-web.md
+++ b/docs/04-cliente-web.md
@@ -300,26 +300,26 @@ visualización de la plataforma:
## 4.6 Rutas
-| Ruta | Página |
-| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `/login` | Inicio de sesión (solo sin sesión) |
-| `/auth/callback` | Retorno de Pireon (PKCE) |
-| `/` | Inicio: saludo según la hora con el resumen del día, «Ir a…» (`WEB-12`), primeros pasos, mis proyectos con su actividad (lo primero, para llegar antes), «Mi día» (`ts-my-day`: los tickets que elijo para hoy, tachados al terminar, mis abiertos para añadir y lo que quedó del último día), mis fijados (`ts-pinned`, una franja que se quita con ×; solo si hay), cuatro cifras (`ts-home-kpis`: abiertos míos, esperando QA, terminados por mí y por el equipo esta semana), la semana (`ts-week-agenda`), mis vistas guardadas y dos columnas: mis tickets y lo último en mis proyectos; sprints en curso, esperando QA, que sigo, próximas releases, quién está ahora y «Retomar» |
-| `/portfolio` | Cartera (con sesión): una tarjeta por proyecto activo con lo abierto y en riesgo, el sprint en curso (¿llega?), las próximas releases con su previsión y las épicas abiertas; en directo: se vuelve a leer sola cuando cambia un ticket, sprint o release de esos proyectos. Cada proyecto lleva su semáforo de salud (`portfolioHealth`), con las causas al pasar por encima |
-| `/explore` | Proyectos públicos (sin sesión) |
-| `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros). Quien puede contribuir cambia prioridad, estimación y persona desde la propia tarjeta (selectores nativos sobre sus distintivos; el título es el enlace). Mover una tarjeta o cambiar su persona ofrece «Deshacer» unos segundos (o Ctrl/⌘ Z): la orden contraria, que el flujo puede rechazar. Con el teclado: Tab entra en las tarjetas, las flechas se mueven entre ellas, Shift+←/→ pasa la tarjeta de columna (mismas reglas que arrastrar), `p`/`e`/`a` abren su prioridad, estimación o persona y Enter la abre |
-| `/p/:key/issues/:issueKey` | Ticket. Encima del título, quién más lo está viendo o editando ahora (`PresenceFacade`: late cada 10 s; editar es tener abierto el formulario del ticket o de un comentario, o estar escribiendo uno). «Fijar» lo deja a mano en el inicio y en la paleta de búsqueda. Arriba, sus decisiones (comentarios marcados con la bandera por quien puede cambiar el ticket); `?comment=
+
+ Desde tu última visita, {{ ago(news.since) }}:
+ {{ news.what }} de {{ news.who }}.
+ @if (news.firstComment; as first) {
+
+