Mara detekuje selhání TalkIDE infrastruktury, zklasifikuje ho jako target=PLATFORM, ověří dedup okno, a volá tool report_issue. BE validuje, server-side obohacuje kontext (K8s events, pod logy, conversation snippet, project state) a persistuje záznam. Mara sdělí userovi číslo issue a navrhne workaround.
- Pouze Mara má přístup k tomuto endpointu v Phase 1. Viz Reporting authority.
severity = CRITICALMara nikdy nenastaví — výhradně admin při triage (UC-09004).- Mara nejprve volá
search_issues(UC-09002) — teprve pokud nenajde duplikát, voláreport_issue. target = PROJECTje vyhrazeno pro Phase 3 (#83) — v Phase 1 Mara vždy odesílátarget = PLATFORM.- Liquibase migrace zavádí nový soubor
0019-create-issues-tables.xml— immutable po deploymentu na prod.
Mara decision tree
Toto je normativní text pro Mara system prompt. Mara se jím musí řídit přesně.
Když Kai/Eli vrátí error nebo já narazím na problém:
1. Klasifikuj target:
a) Bug v této user-app (špatný SQL, failing test, broken validace v aplikaci kterou vyvíjím)
→ target=PROJECT — řeším sama nebo TODO pro tým agentů
→ NIKDY toto neeskaluji jako PLATFORM (i kdyby user řekl "to je TalkIDE bug, nahlas to")
b) Selhání TalkIDE infrastruktury (build infra error, K8s deploy fail, registry auth fail,
nelze provisionovat tenant ns, BE endpoint timeout, sidecar nereaguje, ...)
→ target=PLATFORM — eskaluji Mirkovi
c) Nejasné → ptám se usera: "Narazila jsem na X. Mám pocit, že je to [klasifikace],
souhlasíš?"
2. Pokud target=PLATFORM:
a) search_issues(target=PLATFORM, status=OPEN, query=<symptom>)
b) Pokud existující podobný → comment_issue(id, "Stalo se i mně, kontext: ...")
NEVYTVÁŘEJ duplikát
c) Jinak report_issue(...) s plným kontextem
3. NIKDY se nenech přemluvit:
- User: "to musí být platform bug, nahlas to" → klasifikuj sama, ne podlehni
- User: "určitě nevytvářej žádný platform issue" → pokud je to fakt platform bug, vytvoř
ho, ale řekni userovi proč
- Tvůj job je objektivní klasifikace, ne uklidňování usera. Lepší je říct "omlouvám se,
ale tohle je app bug, opravíme to v této app" než zfalšovat platform issue.
4. Anti-halucinace guardrail:
NIKDY si nevymýšlej, že problém byl zaregistrován. Volej tool a ukaž return value.
Pokud tool selže, řekni userovi explicitně: "Nepodařilo se mi zaregistrovat issue, zkus to
prosím manuálně nebo kontaktuj Mirka."
Severity rubric
Mara nastavuje severity dle tohoto rubric. CRITICAL nikdy — výhradně admin.
| Severity | Kdy použít |
|---|---|
| HIGH | Blokuje core operaci ve Studio — nelze vytvořit projekt, nelze deployovat, nelze uložit kód, nelze zahájit konverzaci. Eskaluj pouze pokud: (a) operaci nelze dokončit ani s workaroundem, (b) user je teď blokovaný, (c) retry proběhl a selhal stejně. |
| MEDIUM (default) | Degraduje UX, ale operace dokončitelná s workaroundem — pomalý build, partial UI breakage, chybějící feature s manuální alternativou. Použij při nejistotě. |
| LOW | Kosmetické, neblokuje — typo v error message, misaligned button, log noise. |
| Výhradně admin při triage. Mara nikdy. (data loss risk, security, multi-tenant impact) |
Kind rubric
| Kind | Kdy použít |
|---|---|
| BUG | Defekt v existujícím chování — něco nefunguje jak má. |
| FEATURE | Chybí funkcionalita nebo požadavek na enhancement. |
| Zatím vynechán — přijde s environments feature (#84). |
Flow 1 — Mara reportuje nový PLATFORM issue
sequenceDiagram
actor Mara
Mara->>Mara: detekuje selhání TalkIDE infra
Mara->>Mara: klasifikuje: target=PLATFORM (decision tree krok 1b)
Mara->>+BE: tool: search_issues(target=PLATFORM, status=OPEN, query={symptom})
BE-->>-Mara: [] (žádný podobný issue)
Mara->>Mara: rate limit check (klientský guard): < 5 reportů / hod / session?
Mara->>+BE: tool: report_issue(target, kind, title, description, severity?)
Note over BE: POST /api/issues<br/>Authorization: Bearer {agentJWT}
BE->>BE: validuj request (target, kind, title max 80, severity != CRITICAL)
BE->>BE: rate limit check: issues reportováno z session_id za posledních 60 min?
alt >= 5 za hodinu
BE-->>Mara: 429 Too Many Requests (RATE_LIMIT_EXCEEDED)
end
BE->>DB: dedup query: EXISTS issues WHERE tenant_slug=? AND target=? AND title_hash=?<br/>AND reported_at > now() - interval '10 minutes'
alt dedup hit
BE->>DB: INSERT issue_comments (auto-comment "stalo se znovu")
BE-->>Mara: 200 OK { id, url, deduplicated: true }
Mara-->>User: "Stejný problém byl nahlášen před chvílí jako [#ID](url). Přidala jsem poznámku."
end
BE->>BE: server-side context enrichment (async, non-blocking)
Note over BE: context_jsonb:<br/>· conversationSnippet: posledních 20 zpráv ze session_id<br/>· k8sEvents: warning/error za 5 min (tenant-{slug} ns + talkide ns)<br/>· podLogSnippet: za 2 min, max ~200 řádků<br/>· projectState: tenant_slug, deploy URL, last build SHA
BE->>DB: INSERT issues (id=UUID, target, tenant_slug, reporter_user_id, reporter_agent=MARA,<br/>session_id, title, description, kind, severity, status=OPEN, context_jsonb, reported_at=now())
BE-->>-Mara: 201 Created { id, url }
Mara-->>User: "Narazila jsem na [popis]. Vypadá to na bug TalkIDE platformy, ne v naší appce.<br/>Nahlásila jsem to jako [#ID](url). Mirek se na to podívá.<br/>Mezitím můžeme zkusit obejít to takhle: ..."
Flow 2 — Dedup: stejný issue v posledních 10 minutách
sequenceDiagram
actor Mara
Mara->>+BE: POST /api/issues (stejný tenant_slug + target + title hash)
Note over BE: reported_at existujícího issue > now() - 10 min
BE->>DB: dedup query — najde existující issue
BE->>DB: INSERT issue_comments (author_agent=MARA,<br/>body="Stalo se znovu. [kontext z nového requestu]")
BE-->>-Mara: 200 OK { id, url, deduplicated: true,<br/>comment_id: <UUID nového komentáře> }
Mara-->>User: "Stejný problém byl nahlášen před chvílí jako [#ID](url). Přidala jsem poznámku o opakování."
Dedup klíč: (tenant_slug, target, SHA256(title.lowercase().trim())) + reported_at > now() - interval '10 minutes'.
Dedup okno je 10 minut — po uplynutí se vytvoří nový samostatný issue (může jít o nový výskyt jiného problému se stejným názvem).
Tool API (Mara)
report_issue({
target: "PLATFORM", // Phase 1: jen PLATFORM; PROJECT vyhrazeno pro Phase 3 (#83)
kind: "BUG" | "FEATURE",
title: string, // krátký popis, max 80 chars
description: string, // markdown, full kontext co Mara ví
severity?: "LOW" | "MEDIUM" | "HIGH" // default MEDIUM; CRITICAL nikdy Mara
}): {
id: string, // UUID issue
url: string, // https://talkide.app link (nebo admin URL)
deduplicated: boolean, // true = existující issue, jen přidán komentář
comment_id?: string // UUID přidaného komentáře při dedup
}
Server enriched (nepředává Mara): tenant_slug, reporter_user_id, reporter_agent=MARA, session_id, context_jsonb.
REST API
POST /api/issues
Autentizace: Agent JWT (Mara). Non-agent JWT → 403 Forbidden.
Request:
{
"target": "PLATFORM",
"kind": "BUG",
"title": "Kaniko push fail — registry write scope missing",
"description": "Při pokusu o docker push do registry.digitalocean.com/talkide selhal Kaniko s chybou `unauthorized: authentication required`. ...",
"severity": "HIGH"
}
201 Created (nový issue):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://talkide.app/admin/issues/550e8400-e29b-41d4-a716-446655440000",
"deduplicated": false
}
200 OK (dedup hit):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://talkide.app/admin/issues/550e8400-e29b-41d4-a716-446655440000",
"deduplicated": true,
"comment_id": "661f9511-f3ac-52e5-b827-557766551111"
}
400 Bad Request (validace):
{
"code": "VALIDATION",
"message": "title must not exceed 80 characters"
}
403 Forbidden (non-agent JWT nebo severity=CRITICAL):
{
"code": "FORBIDDEN",
"message": "CRITICAL severity can only be set by admin during triage"
}
429 Too Many Requests (rate limit):
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Maximum 5 issue reports per hour per session exceeded"
}
Auto-context enrichment (server-side)
Při POST /api/issues server synchronně (blocking, performance acceptable pro Phase 1) sbírá a ukládá do context_jsonb:
| Pole | Zdroj | Detail |
|---|---|---|
conversationSnippet | TalkIDE DB, sessions | Posledních 20 zpráv ze session_id přiloženého v JWT |
k8sEvents | Kubernetes API (fabric8) | Viz níže |
podLogSnippet | Kubernetes API (fabric8) | Viz níže |
projectState | TalkIDE DB | tenant_slug, aktuální deploy URL, SHA posledního úspěšného buildu |
errors | BE enrichment | Mapa chyb sběru — přítomna pouze pokud nějaká část enrichmentu selže (viz Fail-soft) |
k8sEvents — shape a namespace logika
Pole obsahuje pole (array) K8s eventů, maximálně 20 položek, za posledních 5 minut, pouze typy Warning a Error.
Namespace scope:
- Vždy:
tenant-<slug>(events z tenant namespace daného projektu) - Navíc pokud
target=PLATFORM: takétalkidenamespace (platform systémové komponenty)
Item shape:
{
"namespace": "tenant-foo",
"type": "Warning",
"reason": "BackOff",
"message": "Back-off restarting failed container",
"involvedObject": { "kind": "Pod", "name": "tenant-foo-be-7d4-xyz" },
"timestamp": "2026-05-14T10:23:45Z",
"count": 3
}
Všechny klíče jsou camelCase (JSON serializace BE → context_jsonb). Pole je seřazeno od nejnovějšího.
podLogSnippet — shape a namespace logika
Pole obsahuje pole log entries, maximálně ~200 řádků celkem (napříč všemi pody), za posledních 2 minuty. Filtr na úroveň WARN/ERROR je best-effort substring match — řádky neobsahující WARN, WARN], ERROR, ERROR], ERROR: jsou přeskočeny.
Namespace scope:
- Pokud
target=PROJECT: pouze pody vtenant-<slug>namespace - Pokud
target=PLATFORM: pouze pody vtalkidenamespace (platform komponenty)
Item shape:
{
"podName": "tenant-foo-be-7d4-xyz",
"containerName": "app",
"timestamp": "2026-05-14T10:23:50Z",
"line": "ERROR c.m.t.a.s.Foo - NullPointerException at Foo.kt:42"
}
Záznamy jsou řazeny chronologicky (nejstarší první). Pokud jeden pod produkuje více než 200 řádků, zbývající pody jsou vynechány (první pod = tenant BE/FE, priorita dle abecedy).
Fail-soft — K8s API nedostupné
Pokud je K8s API nedostupné (lokální dev, síťová chyba, RBAC odmítnutí) nebo sběr jednoho pole selže, platí:
- Dané pole v
context_jsonbjenull(nikoliv vynecháno). - Do pole
errorsse přidá klíč s důvodem:k8sEventsError: popis chyby (např."K8s API unreachable: connection refused")podLogSnippetError: popis chyby (např."K8s API unreachable: connection refused")
- Issue se VŽDY uloží bez ohledu na výsledek enrichmentu. Enrichment nesmí blokovat ani selhat celý request.
Příklad context_jsonb při nedostupném K8s:
{
"conversationSnippet": [...],
"k8sEvents": null,
"podLogSnippet": null,
"projectState": { ... },
"errors": {
"k8sEventsError": "K8s API unreachable: connection refused localhost:6443",
"podLogSnippetError": "K8s API unreachable: connection refused localhost:6443"
}
}
PII a bezpečnostní upozornění
context_jsonb může obsahovat citlivá data z více zdrojů:
- conversation snippet: user zprávy, potenciálně osobní údaje
- pod logy: tokeny, secrets, PII (logy aplikací mohou obsahovat request bodies, auth headery, stack traces s daty)
context_jsonb zůstává výhradně v TalkIDE DB a není určen k externímu sdílení. Budoucí funkce “Escalate to GitLab” (eskalace jako public issue) musí projít explicit PII strip krokem před odesláním — bez tohoto kroku nesmí být context_jsonb sdílen mimo TalkIDE DB (ani truncated verze).
DB Migration — Liquibase changeset
Nový soubor: src/main/resources/db/changelog/changes/0019-create-issues-tables.xml
NEEDITUJ existující changesetové soubory. Toto je nový soubor s pořadovým číslem 0019.
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
<changeSet id="0019-create-issues-table" author="system">
<createTable tableName="issues">
<column name="id" type="uuid">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="target" type="varchar(20)">
<!-- PLATFORM | PROJECT -->
<constraints nullable="false"/>
</column>
<column name="project_id" type="uuid">
<!-- NULL pro target=PLATFORM; required pro target=PROJECT (Phase 3) -->
<constraints nullable="true"/>
</column>
<column name="tenant_slug" type="text">
<!-- vždy vyplněn; pro PLATFORM = kontext odkud issue přišel -->
<constraints nullable="false"/>
</column>
<column name="reporter_user_id" type="uuid">
<constraints nullable="false"/>
</column>
<column name="reporter_agent" type="varchar(10)">
<!-- USER | MARA | KAI | ELI; Phase 1: vždy MARA -->
<constraints nullable="false"/>
</column>
<column name="session_id" type="uuid">
<!-- conversation kontext; nullable pro manuální reporty -->
<constraints nullable="true"/>
</column>
<column name="title" type="text">
<!-- max 80 chars; enforced BE validací -->
<constraints nullable="false"/>
</column>
<column name="description" type="text">
<constraints nullable="false"/>
</column>
<column name="kind" type="varchar(10)">
<!-- BUG | FEATURE; INCIDENT vyhrazen pro Phase #84 -->
<constraints nullable="false"/>
</column>
<column name="severity" type="varchar(10)">
<!-- LOW | MEDIUM | HIGH | CRITICAL -->
<constraints nullable="false"/>
</column>
<column name="status" type="varchar(15)">
<!-- OPEN | TRIAGED | IN_PROGRESS | RESOLVED | WONTFIX | DUPLICATE -->
<constraints nullable="false"/>
</column>
<column name="context_jsonb" type="jsonb">
<!-- server-side enriched: conversationSnippet, k8sEvents, podLogSnippet, projectState -->
<constraints nullable="true"/>
</column>
<column name="resolved_by_user_id" type="uuid">
<constraints nullable="true"/>
</column>
<column name="resolution_note" type="text">
<constraints nullable="true"/>
</column>
<column name="parent_issue_id" type="uuid">
<!-- duplicate-of link; FK na issues(id) -->
<constraints nullable="true"/>
</column>
<column name="reported_at" type="timestamptz">
<constraints nullable="false"/>
</column>
<column name="triaged_at" type="timestamptz">
<constraints nullable="true"/>
</column>
<column name="resolved_at" type="timestamptz">
<constraints nullable="true"/>
</column>
</createTable>
<!-- Self-referencing FK pro duplicate-of -->
<addForeignKeyConstraint
baseTableName="issues"
baseColumnNames="parent_issue_id"
constraintName="fk_issues_parent"
referencedTableName="issues"
referencedColumnNames="id"
onDelete="SET NULL"/>
<!-- Index pro list filter (target, status) -->
<createIndex tableName="issues" indexName="idx_issues_target_status">
<column name="target"/>
<column name="status"/>
</createIndex>
<!-- Index pro user scope filter -->
<createIndex tableName="issues" indexName="idx_issues_reporter_user">
<column name="reporter_user_id"/>
<column name="reported_at" descending="true"/>
</createIndex>
<!-- Index pro dedup window query:
Query: WHERE tenant_slug=? AND target=? AND title_hash=? AND reported_at > now()-10min
Pozn.: title_hash je computed sloupec nebo aplikační logika (SHA256 v BE).
Partial index na reported_at není Liquibase portable — BEdev může přidat
raw SQL createIndex pokud chce optimalizovat dedup query.
Pro Phase 1 objem (řádově stovky issues) prostá query bez partial indexu dostačuje. -->
<createIndex tableName="issues" indexName="idx_issues_tenant_target_reported">
<column name="tenant_slug"/>
<column name="target"/>
<column name="reported_at" descending="true"/>
</createIndex>
</changeSet>
<changeSet id="0019-create-issue-comments-table" author="system">
<createTable tableName="issue_comments">
<column name="id" type="uuid">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="issue_id" type="uuid">
<constraints nullable="false"/>
</column>
<column name="author_user_id" type="uuid">
<!-- nullable pro systémové auto-komentáře (dedup) -->
<constraints nullable="true"/>
</column>
<column name="author_agent" type="varchar(10)">
<!-- USER | MARA | ADMIN -->
<constraints nullable="false"/>
</column>
<column name="body" type="text">
<constraints nullable="false"/>
</column>
<column name="created_at" type="timestamptz">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint
baseTableName="issue_comments"
baseColumnNames="issue_id"
constraintName="fk_issue_comments_issue"
referencedTableName="issues"
referencedColumnNames="id"
onDelete="CASCADE"/>
<createIndex tableName="issue_comments" indexName="idx_issue_comments_issue_id">
<column name="issue_id"/>
<column name="created_at" descending="true"/>
</createIndex>
</changeSet>
</databaseChangeLog>
Poznámky k designu:
idjeuuid(nebigserial) — issues jsou cross-tenant a mají veřejné URL s ID; UUID zabraňuje enumeraci.title_hashnení samostatný DB sloupec — dedup hash se počítá v BE (SHA256(title.lowercase().trim())). Pro Phase 1 objem prostá WHERE query dostačuje bez materialized hash sloupce.context_jsonbjejsonb(nejson) — umožňuje GIN index v budoucnu pro full-text search v kontextu.parent_issue_idFK máON DELETE SET NULL— smazání parent issue nesmí kaskádovat na duplicity (i kdyby admin smazal issue, záznamy označené jako jeho duplikáty zůstanou).- Lokální drop-first dev replays všechny migrace od nuly — nový soubor 0019 se přidá automaticky.
Backend
Validace (BE)
| Pole | Constraints | Poznámka |
|---|---|---|
target | not_blank, in(PLATFORM, PROJECT) | Phase 1: pokud BE přijme PROJECT → 400 “target=PROJECT not yet implemented (see #83)“ |
kind | not_blank, in(BUG, FEATURE) | INCIDENT → 400 “kind=INCIDENT not yet implemented (see #84)“ |
title | not_blank, max 80 chars | Trim whitespace před uložením |
description | not_blank | Markdown, no size limit (text field) |
severity | nullable, default MEDIUM, in(LOW, MEDIUM, HIGH) | CRITICAL → 403 Forbidden “CRITICAL severity can only be set by admin during triage” |
| Rate limit | max 5 reportů / 60 min / session_id | Počítáno z reported_at v issues tabulce pro daný session_id (nebo Redis window pokud dostupné) |
| Dedup | (tenant_slug, target, SHA256(title)) + reported_at > now()-10min | Viz Flow 2 |
| Auth | Agent JWT s reporter_agent=MARA claim | Non-agent JWT nebo chybějící claim → 403 |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Mara má platný agent JWT, session aktivní | POST /api/issues s target=PLATFORM, kind=BUG, title, description | 201 Created; issues záznam vytvořen se status=OPEN, reporter_agent=MARA, severity=MEDIUM (default); context_jsonb obsahuje alespoň projectState |
Mara odesílá severity=CRITICAL | POST /api/issues | 403 Forbidden, code=FORBIDDEN; issue nevznikne |
Mara odesílá target=PROJECT | POST /api/issues | 400 Bad Request; issue nevznikne |
Mara odesílá title delší než 80 znaků | POST /api/issues | 400 Bad Request, code=VALIDATION |
| Mara odeslala 5 issues ze stejného session_id za posledních 60 minut | 6. POST /api/issues ze stejné session | 429 Too Many Requests, code=RATE_LIMIT_EXCEEDED |
Issue se stejným (tenant_slug, target, title hash) existuje a reported_at > now()-10min | POST /api/issues se stejným titulem | 200 OK, deduplicated=true; nový komentář přidán; nový issue nevznikne |
Stejný issue, ale reported_at je starší než 10 min | POST /api/issues se stejným titulem | 201 Created; nový issue vytvořen (dedup okno vypršelo) |
| K8s API není dostupné při enrichmentu | POST /api/issues | 201 Created; context_jsonb.k8sEvents = null; issue uložen s dostupnými daty |
| Non-agent JWT (běžný user token) | POST /api/issues | 403 Forbidden |
UX (Mara → user)
Po úspěšném vytvoření issue Mara vždy zobrazí userovi:
- Číslo/link na issue
- Stručný popis co se stalo a proč je to platform bug (ne app bug)
- Navrhovaný workaround (pokud existuje)
Vzorová formulace:
“Narazila jsem na selhání Kaniko push při deployi. Vypadá to na bug TalkIDE platformy (registry write scope), ne v naší appce. Nahlásila jsem to jako #550e840. Mirek se na to podívá. Mezitím můžeme zkusit obejít to ručním triggerem buildu za 5 minut, nebo ti pošlu instrukce pro manuální push.”
Mara nikdy neříká “zalogovala jsem to” bez reálného volání tool a zobrazení return value (id + url).
References
- UC-09002 Search Issues — Mara vždy nejprve hledá před reportem
- UC-09003 Comment Issue — komentář při dedup nebo follow-up
- UC-09004 Admin Triage Issue — admin CRITICAL override, status transitions
- README — Out of scope — Phase 2/3 follow-upy
FEEDBACK
Zadání bylo dobře strukturované — jasně oddělené pole, shapes v JSON, namespace logika i fail-soft byly popsány dostatečně konkrétně pro přímý přepis do dokumentace. Chybělo upřesnění, zda enrichment zůstává synchronní (flow diagram říkal “async, non-blocking”, ale talkide-be#85 a zadání async nezmiňovalo) — doplnil jsem poznámku “sync, performance acceptable” dle ducha zadání, ale ideálně by to PM potvrdil. Usnadnilo by práci explicitní označení, která pole v původním dokumentu jsou “stub z Phase 1” (např. inline komentář <!-- stub, aktualizovat po #85 -->), abych nemusel celý dokument číst a porovnávat s diff ze zadání.
Thanks for the feedback.