Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/audit/tracker-gap-reference-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3934,6 +3934,22 @@ Por tanto, «no hay rate limiting configurado» es **falso**; lo correcto es «e
- **Resolution / Next step:** Added `Tracker.Infrastructure/Services/Tenancy/DevTenantSeedHostedService.cs`, mirroring `GeoCatalogSeedHostedService` (scope, log-and-swallow so the API still boots if the DB is down). Registered in `Infrastructure/DependencyInjection.cs`; **gated Development-only** — `if (!environment.IsDevelopment()) return;` is the first line of `StartAsync`, **before any DB access**, so Testing/Prod and the integration-test host (env `Testing`) no-op → clean DB. **Idempotent** (skips if any tenant exists). Seeds 3 **bare** demo tenants (`acme`/`globex`/`initech`) via the real `Tenant.Create` + repository + unit-of-work — no raw SQL, no geo localization (left for the UI, per LV-04..07). Build stays 0 warnings.
- **Status:** `DONE`

#### LV-26

**Title:** An upstream 401 reaches the caller as 502, with the specific code overwritten.

- **Purpose / Problem:** `AgentRuntimeGateway` throws `AgentRuntimeGatewayException(code: "AgentRuntime.HttpError", statusCode: <upstream>)` when the runtime answers non-2xx. `AgentRuntimeTurnExecutor` catches it and flattens it to a STRING (`Result.Failure($"{ex.Code}: {ex.Message}")`) — deliberately, so the failed turn is still recorded, and that intent is right. But the status code does not survive the flattening: `AssistantEndpoints.Translate` re-derives one from the string, matches no case, and falls through `_ => ("AgentRuntime.Failed", 502)`. Two things are lost at once — the **upstream status** (401) and the **specific code**, rewritten from `AgentRuntime.HttpError` to the generic `AgentRuntime.Failed`.
- **What it means:** a CREDENTIAL problem is reported as an AVAILABILITY problem. `502 Bad Gateway` sends the operator to check whether the runtime is up; it was up, and answering 401 in 88 ms. The endpoint's own comment already recognises that collapsing to an HTTP code erases the story and that the trace keeps it — but the caller has no trace, and the caller is who has to act.
- **Example, observed:** on 2026-08-04 the `core-integration` robot reported `POST /assistant/converse → 502` against a live two-cluster stack. The cause was `AGENT_RUNTIME_API_KEY` differing between the Tracker's `tracker-runtime-auth` secret and the runtime's own. Only the tracker-api log revealed the 401; the API surface said bad gateway. Aligning the key turned the step green, so the diagnosis is confirmed and the mis-mapping is what cost the time.
- **Component:** `Backend` · **Module:** Assistant / Agent Runtime integration · **Type:** LV
- **Criticality:** P2 · **Complexity:** XS
- **Discovery:** Found while running `core-integration` against a live stack for the FIRST time — the robot is excluded from the default RoboSoft list and had never executed. The same run found a real Core defect (the MCP chart's `runAsUser`, beyondnetcode/evolith_arch32#425) and one defect in the robot itself (#139).
- **Proposed fix:** Carry the upstream status and code through the `Result` rather than re-deriving them from a string — e.g. a typed failure carrying `(code, upstreamStatus)`, so `Translate` maps `AgentRuntime.HttpError` + 401 to a 502 whose BODY names the upstream status and code. The HTTP status arguably stays 502 (the caller's own auth did succeed; the failure is upstream), so the defect to fix is the ERASURE, not the number.
- **Acceptance criteria:**
- [ ] A non-2xx from the runtime yields a response body naming the upstream status and the specific code, not the generic `AgentRuntime.Failed`.
- [ ] A test drives a 401 from a stubbed runtime and asserts the body distinguishes it from an unreachable runtime.
- **Status:** `PENDING`

#### LV-25

**Title:** Operator tenant switch serves stale, wrong-tenant lists (react-query cache not scoped to the acting tenant).
Expand Down
3 changes: 2 additions & 1 deletion docs/audit/tracker-gap-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor
| [`LV-11`](./tracker-gap-reference-catalog.md#lv-11) | Backend build warnings: 64 × CS0108 (per-aggregate `Id` hiding the Shell.Ddd `Entity` base) + 84 × CS8618 (non-nullable uninitialized) + 2 × CS8620. **Fixed:** 150→0 warnings with real fixes (explicit `new` on intentional `Guid Id` hides, `required` on Props records, element-wise nullability widening at 1 call site); no suppression, 428 tests green. | | | `Backend` | Cross | P2 | M | `DONE` |
| [`LV-12`](./tracker-gap-reference-catalog.md#lv-12) | Frontend lint debt: 39 problems (5 errors + 34 warnings). **Fixed:** `--fix` + manual → **0 errors** (dead code / unused-symbol removal, an equivalent if/else, removed 2 stale eslint-disable comments); 8 `any`/non-null-assertion warnings left by design (fixing = typing refactor). Typecheck still 0. | | | `WEB` | Cross | P3 | S | `DONE` |
| [`LV-13`](./tracker-gap-reference-catalog.md#lv-13) | Dev-seed papercut: the DevBypass platform-root tenant (`11111111…`) is a phantom — no seeder creates it, so local dev must create tenants by hand before tenant-scoped screens work. **Fixed:** `DevTenantSeedHostedService` (Development-only gate before any DB access, idempotent, seeds 3 bare demo tenants via `Tenant.Create` + repo — no geo, left for the UI). Testing/Prod no-op. | | | `Backend/WEB` | Cross | P3 | S | `DONE` |
| [`LV-26`](./tracker-gap-reference-catalog.md#lv-26) | **Un 401 del agent-runtime llega al llamante como 502, y con el código específico sobrescrito.** El gateway lanza `AgentRuntimeGatewayException` con el status real; el executor lo aplana a string a propósito —para que el turno fallido quede asentado, y eso está bien— pero `Translate` vuelve a derivar el status desde ese string, no casa con ningún caso y cae en `_ => ("AgentRuntime.Failed", 502)`. Se pierden a la vez el **status de origen** (401) y el **código específico** (`AgentRuntime.HttpError` → genérico). | Un problema de CREDENCIAL se reporta como problema de DISPONIBILIDAD: el 502 manda a comprobar si el runtime está en pie, y estaba en pie respondiendo 401 en 88 ms. | **Observado 2026-08-04:** el robot `core-integration` dio `POST /assistant/converse → 502` contra el stack real; la causa era `AGENT_RUNTIME_API_KEY` distinta entre el secreto del Tracker y la del runtime. Solo el log de tracker-api mostraba el 401. Alineando la clave el paso pasó a verde. | `Backend` | Cross | P2 | XS | `PENDING` |
| [`LV-25`](./tracker-gap-reference-catalog.md#lv-25) | **Hallado por el nuevo harness UI-E2E (Playwright, `apps/tracker-web-e2e`) — Winston 2026-07-24.** Un platform-operator que cambia el tenant operado seguía viendo las listas tenant-scoped del tenant anterior: varias query-keys de react-query no incluyen el tenant (p.ej. `qk.initiatives = ['initiatives']`, `api/hooks.ts:110`) y `setActingTenant` (`store/auth.store.ts:309`) sólo hacía `set({actingTenantId})` sin invalidar el caché → datos del tenant equivocado hasta `staleTime` (30s), o **indefinidamente** al alternar entre dos tenants con datos. El backend SÍ aísla (robosoft `tenant-isolation` 10/10) — es correctness de **caché de cliente**, no fuga de datos. **Fixed:** efecto en `app/app.tsx` que hace `queryClient.invalidateQueries()` al cambiar `actingTenantId` (guarda de primer render), forzando refetch de toda query activa bajo el nuevo scope. Verificado por el propio E2E: switch a «Acme» → 9 iniciativas + drill-down a las 5 compuertas (`05-processes` 5/5). | Un operador de plataforma cambia de organización y la pantalla sigue mostrando las iniciativas de la anterior | Alternar entre dos tenants con datos mostraba las filas del tenant equivocado hasta 30 s | `WEB` | Cross | P2 | S | `DONE` |
| [`LV-14`](./tracker-gap-reference-catalog.md#lv-14) | La pantalla de Soporte sigue mostrando datos inventados porque no existe modelo de tickets | La sección se ve completa y funcional, pero lo que muestra no corresponde a nada real del negocio | Resuelto por DESCARTE (decisión PO): Soporte no es contexto acotado ni está en la visión; pantalla huérfana del prototipo retirada por completo (pantalla+nav+ruta+mock+permisos). Web typecheck+build verde | `Backend/WEB` | Local | P3 | M | `DONE` |
| [`LV-15`](./tracker-gap-reference-catalog.md#lv-15) | **Hecho — roll-out de [ADR T-034](../adrs/T-034-config-hub-vs-monitor.md)** (Config-hub vs Monitor) completo. Los **monitores** migrados a solo-lectura con la edición movida a zonas inline en Tenant configuration: Gate policies & criteria, Custom fields / artifact schemas (Gate governance 100% solo-lectura) y Tenant intelligence (`intel` solo-lectura). **Connectors (PPM intake)** y **Products** quedan como pantallas de **registro/gestión** — exentas por diseño (ADR T-034 §2.1: los registros no son monitores); sus cards de Config enlazan a su área de gestión ("Manage …"). | | | `WEB` | Cross | P2 | L | `DONE` |
Expand Down Expand Up @@ -221,7 +222,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor
| [`GT-480`](./tracker-gap-reference-catalog.md#gt-480) | El job de despliegue corría también para cambios de sólo documentación | ~14 min de CI para publicar dos ficheros markdown | Sale a su propio workflow con `paths-ignore`; lista negra y no blanca, porque la blanca se queda obsoleta en silencio | `Infra` | Cross | P3 | XS | `DONE` |
| [`GT-481`](./tracker-gap-reference-catalog.md#gt-481) | El despliegue se comprobaba dos veces sobre el mismo árbol | ~7 min de clúster Kubernetes repetidos sobre contenido idéntico | Deja de correr en push a `main`; y las esperas fijas pasan a sondeo por hecho observable | `Infra` | Cross | P3 | XS | `DONE` |

**Progress:** 174 / 203 done · 17 pending · 0 in progress · 3 blocked · 7 deferred · 1 superseded · 1 wontfix
**Progress:** 174 / 204 done · 18 pending · 0 in progress · 3 blocked · 7 deferred · 1 superseded · 1 wontfix
*(Conteos reconciliados contra `python3 .harness/scripts/check-gap-registry.py` el 2026-08-01 al cerrar `CP-01` y `CP-08`: 203 fichas / 203 filas; estados `{PENDING: 17, DONE: 174, DEFERRED: 7, BLOCKED: 3, SUPERSEDED: 1, WONTFIX: 1}`.)*

**Wave 2026-06-07 → 2026-06-14 (BMAD audit + coherence):** Items `GAP-*`, `COH-*`, `OPP-*` from the PROMPT MAESTRO functional/technical/documentary audit and the source-coherence analysis (106 items: 81 resolved, 24 open, 1 blocked, 1 deferred at import time).
Expand Down
26 changes: 22 additions & 4 deletions robosoft/robots/core-integration.robot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,18 @@ export default {
// may be "failed" (the probe manifest has findings) — that is a REAL Core
// evaluation, which is exactly what proves the round-trip.
const data = unwrap(r.body);
check('Core returns a real evaluation verdict (gates evaluated)', r.body?.success === true && Array.isArray(data.gates), {
detail: Array.isArray(data.gates) ? summarizeVerdict(data) : r.body?.error?.message || 'no gates',
// `results.gate`, NOT `gates`. The Core's canonical EvaluationResult nests
// per-kind results under `results` (`gate`, `artifact`, `compliance`); it is
// the TRACKER's own DTO that flattens them to `gates`, and this robot talks
// to the Core through the gateway, so it gets the canonical shape.
//
// Asserting the flattened key made this step report "no gates" against a
// Core that had evaluated six of them — a defect in the instrument that read
// as a defect in the product. Measured against a captured REST response:
// `data.gates` is undefined, `data.results.gate` has 6 entries.
const gates = gatesOf(data);
check('Core returns a real evaluation verdict (gates evaluated)', r.body?.success === true && Array.isArray(gates), {
detail: Array.isArray(gates) ? summarizeVerdict(data) : r.body?.error?.message || 'no gates',
});
}

Expand Down Expand Up @@ -177,8 +187,16 @@ function parseMcpInner(body) {
}

/** One-line summary of an evaluation verdict from the Core's `data` payload. */
/** The canonical EvaluationResult nests gates under `results.gate`. */
function gatesOf(data) {
return Array.isArray(data?.results?.gate) ? data.results.gate : undefined;
}

function summarizeVerdict(data) {
const gates = Array.isArray(data.gates) ? data.gates : [];
const failed = gates.filter((g) => String(g.verdict).toLowerCase() === 'failed').length;
const gates = gatesOf(data) ?? [];
// The Core emits `FAIL`, not `failed`. Comparing against 'failed' reported
// zero failures on a run where all six gates had failed — a green-looking
// summary over a red result, which is worse than no summary.
const failed = gates.filter((g) => /^fail/i.test(String(g.verdict))).length;
return `gates=${gates.length} · failed=${failed}`;
}
Loading