From 61b68eca60e47fe34be2243fced17bb107d6b983 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Tue, 4 Aug 2026 11:24:42 -0500 Subject: [PATCH 1/2] fix(robosoft): core-integration read the Tracker's gate shape off the Core (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 5 reported "no gates" against a Core that had evaluated six of them. The Core was right; the assertion was wrong. The 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 reaches the Core THROUGH the gateway, so it receives the canonical shape and never the flattened one. A second defect sat in the same helper and would have survived the first fix: `summarizeVerdict` compared each verdict against `'failed'` while the Core emits `FAIL`. Even given the right array it would have reported zero failures on a run where all six gates failed — a green-looking summary over a red result, which is worse than no summary at all. Verified against a captured REST response rather than argued, and run against the UNFIXED helper first so the change is known to matter: before gates=0 · failed=0 (and the check said "no gates") after gates=6 · failed=6 Checked for the same shape elsewhere in robosoft/: no other robot reads `.gates` or compares against a lowercase verdict. Found while running this robot against a live two-cluster stack for the first time — it is excluded from the default runner and had never executed. The same run found a real product defect (the MCP chart's runAsUser, fixed in evolith#425); this one was the instrument. Co-authored-by: Claude Opus 5 --- robosoft/robots/core-integration.robot.mjs | 26 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/robosoft/robots/core-integration.robot.mjs b/robosoft/robots/core-integration.robot.mjs index 47904d34..2e59e34d 100644 --- a/robosoft/robots/core-integration.robot.mjs +++ b/robosoft/robots/core-integration.robot.mjs @@ -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', }); } @@ -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}`; } From 53590a08dd0af33aab3aed2971155abd46c19f89 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Tue, 4 Aug 2026 11:37:11 -0500 Subject: [PATCH 2/2] =?UTF-8?q?docs(gaps):=20register=20LV-26=20=E2=80=94?= =?UTF-8?q?=20an=20upstream=20401=20reaches=20the=20caller=20as=20502=20(#?= =?UTF-8?q?140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(robosoft): core-integration read the Tracker's gate shape off the Core Step 5 reported "no gates" against a Core that had evaluated six of them. The Core was right; the assertion was wrong. The 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 reaches the Core THROUGH the gateway, so it receives the canonical shape and never the flattened one. A second defect sat in the same helper and would have survived the first fix: `summarizeVerdict` compared each verdict against `'failed'` while the Core emits `FAIL`. Even given the right array it would have reported zero failures on a run where all six gates failed — a green-looking summary over a red result, which is worse than no summary at all. Verified against a captured REST response rather than argued, and run against the UNFIXED helper first so the change is known to matter: before gates=0 · failed=0 (and the check said "no gates") after gates=6 · failed=6 Checked for the same shape elsewhere in robosoft/: no other robot reads `.gates` or compares against a lowercase verdict. Found while running this robot against a live two-cluster stack for the first time — it is excluded from the default runner and had never executed. The same run found a real product defect (the MCP chart's runAsUser, fixed in evolith#425); this one was the instrument. Co-Authored-By: Claude Opus 5 * docs(gaps): register LV-26 — an upstream 401 reaches the caller as 502 `AgentRuntimeGateway` throws with the upstream status. The executor flattens it to a string so the failed turn is still recorded — that intent is right — but the status does not survive: `Translate` re-derives one from the string, matches no case, and falls through to `_ => ("AgentRuntime.Failed", 502)`. Two things are lost at once: the upstream 401, and the specific code, rewritten to the generic one. A CREDENTIAL problem is therefore reported as an AVAILABILITY problem. 502 sends the operator to check whether the runtime is up; it was up, and answering 401 in 88ms. 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 acts. Observed on 2026-08-04: `core-integration` reported `POST /assistant/converse → 502` against a live two-cluster stack. The cause was AGENT_RUNTIME_API_KEY differing between the Tracker's secret and the runtime's. Only the tracker-api log carried the 401. Aligning the key turned the step green, so the diagnosis is confirmed and the mis-mapping is what cost the time. Registered P2/XS with the fix scoped to the ERASURE, not the number: 502 is arguably right (the caller's own auth succeeded, the failure is upstream) — what is wrong is that the body cannot tell an auth failure from an unreachable runtime. Found running this robot against a live stack for the FIRST time; it is excluded from the default runner. The same run found a real Core defect (evolith_arch32#425) and one in the robot itself (#139). NOTE, pre-existing and untouched: the board's declared counters are stale by five rows. Before this change the header said 174 done / 17 pending while the table held 179 DONE / 12 PENDING; the same offset holds after. This row follows the existing convention (total and pending both +1) rather than silently re-baselining numbers it has not reconciled row-by-row. Worth its own pass — the Core repo has a guard for exactly this and this board has none. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- docs/audit/tracker-gap-reference-catalog.md | 16 ++++++++++++++++ docs/audit/tracker-gap-tracking.md | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index d48b2287..be273506 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -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: )` 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). diff --git a/docs/audit/tracker-gap-tracking.md b/docs/audit/tracker-gap-tracking.md index 5571b0db..f145d87a 100644 --- a/docs/audit/tracker-gap-tracking.md +++ b/docs/audit/tracker-gap-tracking.md @@ -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` | @@ -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).