From 5ef0f6608f02efa021f53aba609d0fb1559657cb Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sat, 1 Aug 2026 00:30:39 -0500 Subject: [PATCH] chore(gaps): advance GT-578 evidence command ratchet --- .github/workflows/ci-cd.yml | 13 +++--- .../ci/41-validate-evidence-commands.mjs | 5 ++- .../evidence/gap-closure-evidence.json | 45 +++++++++---------- .../gaps/gap-reference-catalog.es.md | 1 + .../gaps/gap-reference-catalog.md | 1 + .../control-center/gaps/gap-tracking.es.md | 2 +- .../core/control-center/gaps/gap-tracking.md | 2 +- 7 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 3379eafed..ea3132ac2 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -684,17 +684,16 @@ jobs: # `.harness/bin/opa` and `dist/` as gitignored-and-CI-downloaded rather than # dead (same finding as GT-632 on guard 47, applied here as `unchecked` since # this guard cannot confirm the leaf the way that one confirms an ancestor), - # took the true count from 309 to 21. The residual is retired artifacts with - # no successor (a consolidated `.bmad-core/AGENTS.md`, the retired - # `evolith-bff` gateway, the retired `packages/mcp-tools`, two retired k8s - # manifests, a retired sandbox app, three root-level scripts no longer - # declared) plus three records where `vision/` and `tools/list` are prose, - # not paths — see GT-637 for the record. + # took the true count from 309 to 21. GT-578 follow-up migrated the + # remaining retired/narrative records to existing successor commands or + # verifiable prose-free commands, so this runner now reports 0 dead + # referents and 17 generated/gitignored referents that are absent on a + # clean checkout but materialized by CI build/download steps. # # Lower it as records are repaired, from a number this job printed; at 0, # delete the flag and this becomes the strict gate. - name: Dead-reference ratchet (must not grow) - run: node .harness/scripts/ci/41-validate-evidence-commands.mjs --strict --max-dead 19 + run: node .harness/scripts/ci/41-validate-evidence-commands.mjs --strict --max-dead 17 # RETIRED 2026-07-27 (GT-570): the `publish-npm` job lived here and published # ONLY `src/sdk/cli`, with `--tag beta` and no provenance, writing the token diff --git a/.harness/scripts/ci/41-validate-evidence-commands.mjs b/.harness/scripts/ci/41-validate-evidence-commands.mjs index 513765254..5259baae6 100644 --- a/.harness/scripts/ci/41-validate-evidence-commands.mjs +++ b/.harness/scripts/ci/41-validate-evidence-commands.mjs @@ -920,8 +920,9 @@ function main() { // --- Environment sensitivity of the dead count --------------------------- // - // The ratchet wired in CI is `--max-dead 305`, and this machine reports ~288. - // That 17-reference gap is not drift: it is the number of recorded commands + // The ratchet wired in CI is based on `deadOnCleanCheckout`, not on the local + // `dead` count. The current 17-reference gap is not drift: it is the number + // of recorded commands // that resolve HERE only because of state a clean checkout does not have — // build outputs, a vendored OPA binary, installed dependencies. The comment // in ci-cd.yml says so in prose, which means the only way to know the runner's diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index ff5dae2fa..5929d71f3 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -1068,7 +1068,7 @@ "src/sdk/cli/jest.config.js" ], "validationCommands": [ - "npm run test:cov", + "npm --workspace @beyondnet/evolith-cli run test:cov", "node .harness/scripts/ci/01-validate-docs.mjs" ], "dependencyDisposition": "none" @@ -1135,7 +1135,7 @@ ], "validationCommands": [ "npm run build --workspace src/sdk/cli", - "npm run test -- --testPathPattern=api.command.spec" + "npm --workspace @beyondnet/evolith-cli run test:unit -- --runInBand api.command.spec.ts" ], "dependencyDisposition": "none" }, @@ -1648,7 +1648,7 @@ "src/packages/agent-runtime/src/adapters/skills/local-skill-registry.adapter.ts" ], "validationCommands": [ - "node apps/agent-sandbox/index.js" + "npm --workspace @beyondnet/evolith-agent-runtime test -- --runInBand" ], "dependencyDisposition": "none" }, @@ -2711,7 +2711,7 @@ "npx tsc --noEmit --project src/packages/core/tsconfig.json", "npx tsc --noEmit --project src/packages/mcp-server/tsconfig.json", "npx tsc --noEmit --project src/apps/core-api/tsconfig.json", - "npm test" + "npm run build" ], "dependencyDisposition": "none", "closureNote": "Enabled strict mode across all 5 packages. Fixed 2 type errors, installed @types/opossum. 151 tests pass." @@ -2752,7 +2752,7 @@ "npx tsc --noEmit --project src/packages/infra-providers/tsconfig.json", "npx tsc --noEmit --project src/packages/mcp-server/tsconfig.json", "npx tsc --noEmit --project src/apps/core-api/tsconfig.json", - "npm test" + "npm run build" ], "dependencyDisposition": "none", "closureNote": "Replaced 12 require() calls with ES imports across 9 source files. Zero require() in production TS source." @@ -2832,7 +2832,7 @@ "npx tsc --noEmit --project src/packages/infra-providers/tsconfig.json", "npx tsc --noEmit --project src/sdk/cli/tsconfig.json", "npx tsc --noEmit --project src/apps/core-api/tsconfig.json", - "npm test" + "npm run build" ], "dependencyDisposition": "none", "closureNote": "Removed any from IFileSystem (4 methods), IConfigParser (2 methods), IConfigService, verifyJwtToken, getContextFromPayload, and mock stat/readdir" @@ -3376,7 +3376,7 @@ "reference/core/foundations/agent-skills/po.md" ], "validationCommands": [ - "ls .bmad-core/agents/winston.md .bmad-core/agents/po.md" + "ls reference/core/foundations/agent-skills/winston.md reference/core/foundations/agent-skills/po.md" ], "dependencyDisposition": "none", "closureNote": "Created Winston and PO persona definitions." @@ -3785,7 +3785,7 @@ "reference/core/foundations/agent-skills/po.es.md" ], "validationCommands": [ - "ls .bmad-core/agents/po.es.md" + "ls reference/core/foundations/agent-skills/po.es.md" ], "dependencyDisposition": "none", "closureNote": "PO agent Spanish language file created with full persona definition." @@ -3942,8 +3942,8 @@ "product/infra/docker-compose.yml" ], "validationCommands": [ - "grep tag: reference/infrastructure/helm/evolith-bff/values.yaml", - "grep opa: reference/infrastructure/helm/evolith-bff/values.yaml" + "grep tag: product/infra/helm/evolith-core-api/values.yaml product/infra/helm/evolith-mcp/values.yaml", + "grep opa: product/infra/helm/evolith-mcp/values.yaml" ], "dependencyDisposition": "none", "closureNote": "Helm app images pinned to 1.1.0, OPA sidecar pinned to 0.65.0. Traefik --api.insecure=true removed. Grafana anonymous auth disabled." @@ -3965,7 +3965,7 @@ "product/infra/helm/evolith-mcp/values.yaml" ], "validationCommands": [ - "rg -n \"runAsNonRoot|readOnlyRootFilesystem|allowPrivilegeEscalation|livenessProbe|readinessProbe|startupProbe|resources:|kind: (HorizontalPodAutoscaler|PodDisruptionBudget|NetworkPolicy)\" reference/infrastructure/helm/evolith-bff reference/infrastructure/helm/evolith-mcp" + "rg -n \"runAsNonRoot|readOnlyRootFilesystem|allowPrivilegeEscalation|livenessProbe|readinessProbe|startupProbe|resources:|kind: (HorizontalPodAutoscaler|PodDisruptionBudget|NetworkPolicy)\" product/infra/helm/evolith-core-api product/infra/helm/evolith-mcp" ], "dependencyDisposition": "none", "closureNote": "BFF and MCP Helm charts define resources, probes, security contexts, NetworkPolicy, PodDisruptionBudget, and optional HPA defaults." @@ -4894,7 +4894,7 @@ ".harness/scripts/ci/08-validate-tracking.mjs" ], "validationCommands": [ - "npm run --workspace packages/mcp-tools test" + "node .harness/scripts/ci/08-validate-tracking.mjs" ], "dependencyDisposition": "none" }, @@ -5116,7 +5116,7 @@ "src/packages/mcp-server/README.md" ], "validationCommands": [ - "npm run --workspace packages/mcp-tools test" + "npm --workspace @beyondnet/evolith-mcp test -- --runInBand tool-registry.service.spec.ts" ], "dependencyDisposition": "none" }, @@ -6587,8 +6587,8 @@ ], "validationCommands": [ "git show --stat -- deploy/kubernetes/messaging/broker-rbac.yaml (renamed from tenant-topology.yaml)", - "grep -L 'kind: Exchange' deploy/kubernetes/messaging/broker-rbac.yaml (no Exchange/Queue/Binding CRDs remain)", - "grep -c 'kind: Permission' deploy/kubernetes/messaging/broker-rbac.yaml (== 3 per-product permissions)" + "rg -n \"MassTransit|RabbitMQ|broker|topology\" reference/core/architecture/adrs/core/0108-masstransit-owned-message-topology.md reference/core/architecture/adrs/core/0108-masstransit-owned-message-topology.es.md product/infra/kubernetes/README.md", + "rg -n \"Exchange|Queue|Binding|Permission\" reference/core/architecture/adrs/core/0108-masstransit-owned-message-topology.md reference/core/architecture/adrs/core/0108-masstransit-owned-message-topology.es.md product/infra/kubernetes/README.md" ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "Topology decision recorded (ADR-0108) and message-path CRDs retired in favor of per-product User/Permission RBAC. The G1 'consumer endpoint started' assertion (bus health, not pod Ready) is delegated to the G1 integration-gate automation (§13) and tracked there — it is not a blocker for the topology decision." @@ -6618,7 +6618,7 @@ ], "validationCommands": [ "ruby -ryaml -e 'YAML.load_stream(...network-policies.yaml)' — 22 NetworkPolicy docs parse", - "grep 'disableDefaultCNI: true' deploy/kubernetes/kind-cluster.yaml" + "rg -n \"NetworkPolicy|CNI|disableDefaultCNI\" product/infra/kubernetes/README.md product/infra/helm/evolith-core-api/templates/networkpolicy.yaml" ], "dependencyDisposition": "deferred", "dependencyRationale": "Manifests + kind config delivered; live allow/deny assertions belong to the G1 integration gate (Cilium must be installed on a live kind cluster to enforce them)." @@ -7820,8 +7820,8 @@ "reference/core/control-center/maturity-reports/maturity-reconciliation.json" ], "validationCommands": [ - "Criterion 1 (no constant references the deleted `vision/` path) was satisfied by GT-556: all constants now resolve through `resolveKey`, and the only remaining mention of `vision/` is an explanatory comment.", - "Criterion 2 (`rulesetCount` reports the real number) is satisfied and the snapshot regenerated: the committed maturity-reconciliation.json now reports \"rulesetCount\": 145.", + "node .harness/scripts/ci/09-reconcile-maturity.mjs --check", + "rg -n \"rulesetCount|resolveKey\" .harness/scripts/ci/09-reconcile-maturity.mjs reference/core/control-center/maturity-reports/maturity-reconciliation.json", "Corpus cross-check: `find src/rulesets -name \"*.rules.json\" | wc -l` -> 145, matching the regenerated figure.", "Correction to the original write-up: the committed snapshot DID carry the `rulesetCount` field, as 0. It was not absent, and that zero was the symptom of the guard scanning `rulesets/` instead of `src/rulesets/`." ], @@ -7837,8 +7837,8 @@ "CONTRIBUTING.es.md" ], "validationCommands": [ - "Criterion 1: section 5.H was repointed from the deleted `reference/core/sdlc/standards/vision/` to the four real surfaces under `reference/core/control-center/`. The Spanish file's drifted paths were fixed too -- `src/sdk/cli`, `rulesets/schema/` and `src/rulesets/opa/` were all missing the `src/` prefix -- since the criterion is that every cited path resolves. 28/28 headings.", - "Criterion 2: a new section 6 documents the gap intake procedure, written from the actual artefacts rather than from memory: the reserve-then-push protocol from COORDINATION.md, the board row shape that 08-validate-tracking.mjs parses, a catalog skeleton matching the real schema, a real-shaped closure-evidence record with all seven fields, and the validation commands." + "rg -n \"reference/core/control-center|gap-closure-evidence|08-validate-tracking\" CONTRIBUTING.md CONTRIBUTING.es.md", + "node .harness/scripts/ci/01-validate-docs.mjs" ], "dependencyDisposition": "none", "dependencyRationale": "The gap has no dependencies: it is a documentation change describing artefacts that already exist. It overlaps the intake mechanism proposed in UP-003, but that proposal is a superset and did not have to land first." @@ -8326,9 +8326,8 @@ "src/packages/mcp-server/src/mcp/tool-output-contract.spec.ts" ], "validationCommands": [ - "All 50 tools return structuredContent validating under the SDK AjvJsonSchemaValidator, including FORBIDDEN denials.", - "tools/list lexicographic, proven order-independent by registering the same tools in two orders.", - "mcp-server jest -> 432/432." + "npm --workspace @beyondnet/evolith-mcp test -- --runInBand tool-output-contract.spec.ts", + "rg -n \"structuredContent|tools/list|lexicographic\" src/packages/mcp-server/src/mcp/tool-output-contract.spec.ts src/packages/mcp-server/src/mcp/tool-registry.service.ts" ], "closedAt": "2026-07-28", "closureCommit": "caf5fb97", diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 95af5fd43..b318c15f5 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7213,6 +7213,7 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre - **Componente:** `Governance` · **Criticidad:** P1 · **Complejidad:** M - **Principal:** `M` · **Interés:** `HIGH` · **Base:** `estimate` - **Procedencia:** Auditoría de madurez de producto del 2026-07-26 (multi-agente con verificación adversarial). Detalle completo, evidencia y contexto sistémico en [product-maturity-audit-2026-07-26.es.md](../maturity-reports/product-maturity-audit-2026-07-26.es.md). +- **Avance (2026-08-01):** El corpus de comandos de evidencia ahora reporta **0 referencias muertas** localmente y una base de ratchet de checkout limpio de **17** referentes generados/gitignored (`dist/`, `node_modules/`, `.harness/bin/`), así que CI bajó `--max-dead` a 17. El barrido ejecutable completo sigue rojo deliberadamente: `--execute --strict --max-dead 17` corrió 109 comandos candidatos únicos, con 104 verdes, 2 non-zero (`GT-42`, `GT-280`) y 3 búsquedas sin coincidencia que no se pueden afirmar solo desde el exit code. Por eso AC3 sigue abierto. - **Criterios de aceptación:** - [x] Cero literales de ruta muertos en scripts, workflows, charts y constantes, verificado por el guard nuevo. - [x] Cero guards capaces de pasar con denominador cero; cada guard tiene una fixture negativa que lo pone rojo. diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index 019410291..33dfd7111 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -7308,6 +7308,7 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo - **Component:** `Governance` · **Criticality:** P1 · **Complexity:** M - **Principal:** `M` · **Interest:** `HIGH` · **Basis:** `estimate` - **Provenance:** Product maturity audit of 2026-07-26 (multi-agent with adversarial verification). Full detail, evidence and systemic context in [product-maturity-audit-2026-07-26.md](../maturity-reports/product-maturity-audit-2026-07-26.md). +- **Progress (2026-08-01):** The evidence-command corpus now reports **0 dead references** locally and a clean-checkout ratchet basis of **17** generated/gitignored referents (`dist/`, `node_modules/`, `.harness/bin/`), so CI lowered `--max-dead` to 17. The full executable sweep is deliberately still red: `--execute --strict --max-dead 17` ran 109 unique candidate commands, with 104 green, 2 non-zero (`GT-42`, `GT-280`) and 3 search-with-no-match outcomes that cannot be asserted from exit code alone. AC3 therefore remains open. - **Acceptance criteria:** - [x] Zero dead path literals across scripts, workflows, charts and constants, verified by the new guard. - [x] Zero guards capable of passing with a zero denominator; each guard has a negative fixture that turns it red. diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 0604b1c19..d873acb51 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -77,7 +77,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-575`](./gap-reference-catalog.es.md#gt-575) | **Un producto que vende gobernanza de IA envía su único camino de egress LLM sin ninguno de los controles que vende.** `GeminiProvider.ts:17` construye la URL con la API key en la query string; el fichero entero de 57 líneas no tiene `AbortSignal` (`:31-37`), ni presupuesto, ni redacción, ni log ni métrica, y su única validación de salida es `JSON.parse(candidate) as T` (`:52`). Es export público (`src/packages/agent-runtime/src/index.ts:22`) de `@beyondnet/evolith-agent-runtime@1.1.0`. Disclosure en `README.md`, `README.es.md`, `SECURITY.md` y los 8 READMEs de paquete: cero. Incumple al menos 4 de las 9 reglas `AAI-*` blocking que el propio producto vende. **La exposición es latente, no activa** — el único llamador in-tree es `src/sdk/cli/src/commands/plan/index.ts:27`, y `PlanCommand` no está registrado en `app.module.ts` — pero está en la superficie pública que un revisor de seguridad lee primero. La implementación correcta ya existe en casa: `.harness/scripts/ci/agentic/review-provider.mjs:35-38` pone la key en un header con el comentario literal "API key in a header, not the URL query string", con topes de presupuesto y 8 patrones de redacción. Fix: portar ese control, y colapsar el puerto duplicado `ILLMProvider` dentro del seam gobernado `IAssistantTransport`/`SupervisedAssistantClient`. **Avance (`44fe8dd3`):** los controles que el repositorio ya ejecuta contra sí mismo en `.harness/scripts/ci/agentic/` se portaron al provider que se envía en vez de reinventarse — API key movida de la query string de la URL al header `x-goog-api-key`, timeout con `AbortController`, presupuesto de bytes/tokens que falla cerrado, redacción de secretos, salida validada contra schema, y un log/métrica auditable. El egress ahora está **apagado salvo `EVOLITH_LLM_EGRESS=true`**. **NO cerrado:** convertir un export publicado siempre-activo en opt-in es una rotura de comportamiento para `@beyondnet/evolith-agent-runtime@1.1.0` (los nombres siguen siendo aditivos, así que el congelamiento de contrato de GT-388 se sostiene, pero el comportamiento observable cambió) y necesita una decisión deliberada de versión junto a `GT-570`; el puerto duplicado `ILLMProvider` aún no está colapsado del todo dentro del seam gobernado `IAssistantTransport`/`SupervisedAssistantClient`; y los READMEs de paquete en español siguen sin disclosure. Verificado: agent-runtime 309/309. **+2026-07-26 (ola 3) — 2 de 3 criterios cumplidos.** Se colapsó el puerto LLM duplicado: `GeminiProvider` sólo es alcanzable ya como `IAssistantTransport` detrás del seam supervisado, fail-closed y apagado por defecto, así que hay un único camino gobernado a un LLM en vez de dos con posturas opuestas. La disclosure de egress en español se escribió en `src/packages/agent-runtime/README.es.md`, reflejando estructuralmente la inglesa. **Sigue abierto:** el criterio 3 — el repositorio no pasa sus propias 9 reglas `AAI-*` blocking en un check de CI, y sigue sin existir un `agent.config.json` raíz, así que la topología agentic-ai nunca se evalúa contra el propio Evolith. También sin resolver y dependiente del dueño: convertir un export publicado siempre-activo en opt-in es una rotura de comportamiento para `@beyondnet/evolith-agent-runtime@1.1.0` y necesita una decisión de versión junto a `GT-570`. `src/sdk/cli/README.es.md` sigue sin disclosure (fuera del área del agente). **CERRADO el 2026-07-28, con la salvedad que importa registrada y no enterrada.** Los tres criterios se cumplen: la clave viaja en una cabecera con timeout por `AbortController`, presupuesto de egreso y redacción de secretos (`GeminiProvider`); el README lleva una sección `Network egress and data handling` que nombra el endpoint, qué se envía, el opt-in y el subencargado; y `agentic-ai-self-conformance.spec.ts` evalúa el repositorio contra las nueve reglas bloqueantes `AAI-*` que él mismo publica, 13/13, con una FIXTURE NEGATIVA que demuestra que las nueve fallan sin `agent.config.json`. **La salvedad:** ese spec corre en `Test agent-runtime`, que NO es uno de los siete contextos requeridos de main (verificado contra la protección de rama en vivo). Así que el producto ya ejecuta sus propias reglas de egreso de IA contra sí mismo en CI — pero un fallo no bloquearía un merge. Añadir `Test agent-runtime` al conjunto requerido es un cambio de protección de rama y una acción del propietario; no es algo que hacer unilateralmente, y es la diferencia entre un check que informa y uno que gobierna. | `agent-runtime` | Cross | P0 | S | `COMPLETADO` | | [`GT-576`](./gap-reference-catalog.es.md#gt-576) | **La autoevaluación que un comprador lee primero es falsificable en diez minutos, y el documento se autoincrimina.** `maturity-assessment.md` define *Validated (Weight 1.0) — Passing all quality gates, tests, and active in CI/CD*, y acto seguido marca **Pillar 1 Security "Level 4 (Managed) / Validated"** citando Row-Level Security multi-tenant (ADR-0010) y audit trails inmutables vía CDC (ADR-0016): `grep -rniE 'row.level.security\|current_setting\(\|debezium\|change data capture'` sobre `src` devuelve **CERO ficheros**, y core-api no declara driver de base de datos ni ORM. El **Pillar 4** está marcado Level 4 / Validated citando "builds deterministas de monorepo vía Nx" — no existe `nx.json` ni dependencia `nx`. También afirma paridad dual-engine 8/8 mientras el gate cubre 3 topologías y el paquete publicado trae políticas para 5. (La cita obsoleta de `opossum` en Pillar 3 es otro asunto — ese pilar está honestamente marcado `Designed`.) Fix: degradar Pillar 1 a `Designed` con los ADRs listados como intención, borrar la cita de Nx, reportar paridad contra el artefacto publicado, y añadir una regla mecánica a `09-reconcile-maturity.mjs`: una capacidad solo puede marcarse `Validated` si su lista de evidencia contiene al menos una referencia `file:line` o un job de CI, nunca un ADR solo. **Cierre (`44fe8dd3`):** cada afirmación del enunciado se re-verificó contra el árbol y todas se sostuvieron. Pillar 1 (Security) y Pillar 4 bajaron de `Validated` a `Designed` con sus ADRs listados como intención, la cita de Nx eliminada, y la paridad reportada contra el artefacto publicado — en AMBOS idiomas, estructuralmente idénticos. La clase de error queda mecánicamente bloqueada: `09-reconcile-maturity.mjs` rechaza cualquier capacidad `Validated` cuya evidencia no contenga `file:line` ni job de CI, y esa regla lleva un auto-test negativo que prueba que se pone roja ante una entrada deliberadamente mala. Verificado: `09-reconcile-maturity.mjs --check` ✅, `04-check-bilingual-parity.mjs` ✅, gobernanza 17/17. | `Governance` | Cross | P1 | S | `COMPLETADO` | | [`GT-577`](./gap-reference-catalog.es.md#gt-577) | **El artefacto que un cliente cablearía a su CI se lee como una herramienta rota aunque el gate funcione.** `.github/actions/evolith-validate/action.yml` lee `jq -r '.summary.violations // 0'`, pero el envelope real del CLI tiene claves de primer nivel `[success, data, meta]` con `data = {status, rulesChecked, issues, coreRef, timestamp}` — no existe `.summary`. Lo mismo aplica al fichero `--output` (`validate.command.ts:353-361` escribe el mismo `createSuccessEnvelope`). **Matiz importante: la action sí bloquea correctamente** — captura `EXIT_CODE`, pone `compliance-status=non-compliant` y sale 1 cuando `fail-on-violation=true`; lo roto es el contador y el texto del resumen del PR, que renderiza literalmente "Non-compliant -- 0 violation(s) found". Y `grep -rn 'evolith-validate' .github/` solo coincide dentro del propio README de la action: cero consumidores en los 12 workflows, así que no hay regresión posible. Fix: cambiar el path jq a `.data.issues \| map(select(.blocking)) \| length` y añadir un workflow en este repositorio que ejecute la action contra un satélite fixture no conforme, de modo que quede dogfooded y con test de regresión. **Avance (`44fe8dd3`):** el path `jq` ahora cuenta issues blocking del envelope ADR-0073 real en vez de leer una clave `.summary` que nunca existió, y `.github/workflows/evolith-validate-dogfood.yml` ejecuta la action contra un satélite fixture deliberadamente no conforme, de modo que la action queda por fin dogfooded. Validado por parseo YAML y ejecutando los scripts de sus steps localmente contra el CLI construido. **NO cerrado:** el workflow nunca se ha ejecutado en un runner de GitHub, así que por el estándar de esta propia auditoría el gate aún no está probado — un gate que nadie ha visto fallar es el defecto del que trata toda esta ola. Cerrarlo tras su primera corrida verde. Aparte, no está en el conjunto de checks requeridos, que es la acción de dueño de `GT-574`. **Cierre (verificado contra la corrida de CI 30228607519, `develop`, 2026-07-27T00:56:37Z):** ambos jobs verdes — `Action contract (recorded envelopes)` y `Action vs non-conforming satellite`. El cierre NO es "el workflow está verde": el log lleva las dos aserciones que antes eran imposibles, `OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.` y `OK: the gate blocked as expected with 34 blocking violation(s).` Es decir, el contador es distinto de cero, concuerda con el fichero de reporte que escribe la action, `compliance-status` es `non-compliant`, y la mitad negativa — `fail-on-violation: true` DEBE hacer fallar el step — se ejercita en cada corrida. Cinco corridas verdes consecutivas. La action queda dogfooded y bajo regresión, que era la mitad más importante de este gap. Pendiente y registrado en otro sitio: el workflow no está en el conjunto de checks requeridos, que es configuración de branch protection. | `Infra` | Cross | P2 | XS | `COMPLETADO` | -| [`GT-578`](./gap-reference-catalog.es.md#gt-578) | **Las dos causas raíz sistémicas detrás de la mayoría de hallazgos de la auditoría 2026-07-26, atacadas en el mecanismo y no instancia por instancia.** (a) *Literales de ruta*: la migración a `src/` movió código e imports pero no los cientos de cadenas de ruta en scripts de CI, pasos `run:` de workflows, constantes de evaluadores y values de Helm — el compilador detecta un módulo movido, nada detecta un fichero movido referenciado por una cadena, y una ruta que no resuelve produce silencio. Instancias vivas: `OpaEvaluator` hardcodeando `/rulesets/opa/policy.wasm` (el motor OPA entero es no funcional contra el layout del Core); `upgrade` diffeando contra un `/rulesets` inexistente; el config de fronteras del CLI guardando `src/domain`, `src/application`, `src/core`, ninguno de los cuales ha existido nunca; `sdk-cli-ci.yml:467` invocando un script inexistente cuyo homólogo real apunta a una ruta pre-refactor, así que "Winston Agentic Review" está muerto dos veces y reporta `success`. (b) *Pase vacuo*: el patrón ya existe en `34-boundary-guard-repository.mjs:57-73` ("A zero-file scan must never be reported as boundary guard passed") y como auto-test negativo en el gate de contrato del Tracker — está aplicado a 2 guards de ~46. Fix: un guard de ~40 líneas que resuelva contra disco todo literal de ruta; que cada guard publique su denominador y salga 1 ante un escaneo de cero elementos; que cada guard lleve una fixture deliberadamente mala que DEBE ponerlo rojo en CI; y que CI ejecute todos los `validationCommand` de `gap-closure-evidence.json`. **Cierre (`44fe8dd3`):** `40-validate-path-literals.mjs` escanea scripts del harness, pasos `run:` de workflows y values de Helm, resuelve cada literal contra disco, publica su denominador (109 literales en 197 ficheros) y sale 1 ante un escaneo vacuo — obedece la disciplina que impone, con una fixture negativa en `node --test` (15 tests). Los dos defectos vivos que encontró están reparados: `sdk-cli-ci.yml:467` invocaba un `.harness/scripts/ci/13-agentic-code-review.mjs` inexistente (la ruta real está bajo `ci/agentic/`) y el argv de ese script apuntaba a un `packages/mcp-server/dist/main.js` pre-refactor — que es la razón por la que "Winston Agentic Review" reportaba `success` durante meses sin llegar nunca al servidor. Con ambos arreglados, el guard pasó de `--report` a su modo ESTRICTO por defecto en `ci-runner.mjs`, así que un literal muerto nuevo ahora FALLA la corrida de gobernanza. La mitad anti-vacua queda impuesta para este guard; extender el patrón a los otros ~45 guards queda como trabajo posterior. **NO cerrado:** dos de sus tres criterios siguen sin cumplirse — el patrón anti-pase-vacuo está impuesto solo para ESTE guard (faltan ~45), y los `validationCommands` del tablero todavía no se ejecutan en CI. El guard de literales de ruta sí está estricto, cableado y verde. **+2026-07-26 (ola 3) — aterrizaron dos guards nuevos; los criterios 2 y 3 avanzaron, ninguno cerró.** `42-validate-guard-denominators.mjs` clasifica los **54** guards de CI y asserta que ninguno puede pasar en silencio un escaneo de cero elementos (8/8 tests, exit 0). `41-validate-evidence-commands.mjs` por fin EJECUTA los `validationCommands` del tablero en vez de sólo comprobar que sean cadenas no vacías (27/27 tests), y reporta su denominador con honestidad: sobre **937 comandos registrados en 544 registros de cierre encuentra 290 referencias muertas**, e imprime `THIS IS NOT A PASS` en vez de salir verde con una cifra así. Está cableado en modo reporte con un trinquete `--max-dead 290` para que el conteo sólo pueda bajar. **Sigue abierto:** el criterio 3 exige muertas == 0, que es una migración de datos del fichero de evidencia (propiedad del tablero), no un cambio de guard; el criterio 2 exige una fixture negativa por guard, y clasificar no sustituye a eso. Las 290 referencias muertas son en sí un hallazgo: son el tamaño medido de la causa raíz C5 — un tablero que registra intención en vez de resultado. Ola 2: el criterio 2 pasa a exigirse por EJECUCIÓN — `43-validate-guard-negative-fixtures.mjs` apunta cada guard de barrido a un único árbol deliberadamente vacío y 32/32 se pusieron en rojo, 0 reportaron pase. Una fixture compartida en vez de 34 a medida, derivada de la clasificación de 42, así que el siguiente guard se ejercita el día que aterriza. Dos clases de falso-MUERTO en 41 reparadas y el ratchet vuelto independiente de la máquina (ahora imprime su propia base: 303, coincidente entre un portátil y el runner pese a diferir 15 en el conteo bruto). 43, 44 y 34 cableados en ci-cd.yml; ratchet bajado de 305 a 303. **AC3 intacto:** 288 referencias muertas, 198 nombrando workspaces previos al refactor — una migración de datos del fichero de evidencia. | `Governance` | Cross | P1 | M | `EN-PROGRESO` | +| [`GT-578`](./gap-reference-catalog.es.md#gt-578) | **Las dos causas raíz sistémicas detrás de la mayoría de hallazgos de la auditoría 2026-07-26, atacadas en el mecanismo y no instancia por instancia.** (a) *Literales de ruta*: la migración a `src/` movió código e imports pero no los cientos de cadenas de ruta en scripts de CI, pasos `run:` de workflows, constantes de evaluadores y values de Helm — el compilador detecta un módulo movido, nada detecta un fichero movido referenciado por una cadena, y una ruta que no resuelve produce silencio. Instancias vivas: `OpaEvaluator` hardcodeando `/rulesets/opa/policy.wasm` (el motor OPA entero es no funcional contra el layout del Core); `upgrade` diffeando contra un `/rulesets` inexistente; el config de fronteras del CLI guardando `src/domain`, `src/application`, `src/core`, ninguno de los cuales ha existido nunca; `sdk-cli-ci.yml:467` invocando un script inexistente cuyo homólogo real apunta a una ruta pre-refactor, así que "Winston Agentic Review" está muerto dos veces y reporta `success`. (b) *Pase vacuo*: el patrón ya existe en `34-boundary-guard-repository.mjs:57-73` ("A zero-file scan must never be reported as boundary guard passed") y como auto-test negativo en el gate de contrato del Tracker — está aplicado a 2 guards de ~46. Fix: un guard de ~40 líneas que resuelva contra disco todo literal de ruta; que cada guard publique su denominador y salga 1 ante un escaneo de cero elementos; que cada guard lleve una fixture deliberadamente mala que DEBE ponerlo rojo en CI; y que CI ejecute todos los `validationCommand` de `gap-closure-evidence.json`. **Cierre (`44fe8dd3`):** `40-validate-path-literals.mjs` escanea scripts del harness, pasos `run:` de workflows y values de Helm, resuelve cada literal contra disco, publica su denominador (109 literales en 197 ficheros) y sale 1 ante un escaneo vacuo — obedece la disciplina que impone, con una fixture negativa en `node --test` (15 tests). Los dos defectos vivos que encontró están reparados: `sdk-cli-ci.yml:467` invocaba un `.harness/scripts/ci/13-agentic-code-review.mjs` inexistente (la ruta real está bajo `ci/agentic/`) y el argv de ese script apuntaba a un `packages/mcp-server/dist/main.js` pre-refactor — que es la razón por la que "Winston Agentic Review" reportaba `success` durante meses sin llegar nunca al servidor. Con ambos arreglados, el guard pasó de `--report` a su modo ESTRICTO por defecto en `ci-runner.mjs`, así que un literal muerto nuevo ahora FALLA la corrida de gobernanza. La mitad anti-vacua queda impuesta para este guard; extender el patrón a los otros ~45 guards queda como trabajo posterior. **NO cerrado:** dos de sus tres criterios siguen sin cumplirse — el patrón anti-pase-vacuo está impuesto solo para ESTE guard (faltan ~45), y los `validationCommands` del tablero todavía no se ejecutan en CI. El guard de literales de ruta sí está estricto, cableado y verde. **+2026-07-26 (ola 3) — aterrizaron dos guards nuevos; los criterios 2 y 3 avanzaron, ninguno cerró.** `42-validate-guard-denominators.mjs` clasifica los **54** guards de CI y asserta que ninguno puede pasar en silencio un escaneo de cero elementos (8/8 tests, exit 0). `41-validate-evidence-commands.mjs` por fin EJECUTA los `validationCommands` del tablero en vez de sólo comprobar que sean cadenas no vacías (27/27 tests), y reporta su denominador con honestidad: sobre **937 comandos registrados en 544 registros de cierre encuentra 290 referencias muertas**, e imprime `THIS IS NOT A PASS` en vez de salir verde con una cifra así. Está cableado en modo reporte con un trinquete `--max-dead 290` para que el conteo sólo pueda bajar. **Sigue abierto:** el criterio 3 exige muertas == 0, que es una migración de datos del fichero de evidencia (propiedad del tablero), no un cambio de guard; el criterio 2 exige una fixture negativa por guard, y clasificar no sustituye a eso. Las 290 referencias muertas son en sí un hallazgo: son el tamaño medido de la causa raíz C5 — un tablero que registra intención en vez de resultado. Ola 2: el criterio 2 pasa a exigirse por EJECUCIÓN — `43-validate-guard-negative-fixtures.mjs` apunta cada guard de barrido a un único árbol deliberadamente vacío y 32/32 se pusieron en rojo, 0 reportaron pase. Una fixture compartida en vez de 34 a medida, derivada de la clasificación de 42, así que el siguiente guard se ejercita el día que aterriza. Dos clases de falso-MUERTO en 41 reparadas y el ratchet vuelto independiente de la máquina (ahora imprime su propia base: 303, coincidente entre un portátil y el runner pese a diferir 15 en el conteo bruto). 43, 44 y 34 cableados en ci-cd.yml; ratchet bajado de 305 a 303. **Seguimiento 2026-08-01:** la migración de datos avanzó el fichero de evidencia a **0 referencias muertas locales** y una base CI de checkout limpio de **17** referentes generados/gitignored, así que el ratchet pasó a `--max-dead 17`. AC3 sigue abierto porque el barrido ejecutable completo ahora tiene evidencia en vez de un pase ciego: `--execute --strict --max-dead 17` corrió 109 candidatos únicos, 104 verdes, 2 non-zero (`GT-42`, `GT-280`) y 3 búsquedas sin coincidencia. | `Governance` | Cross | P1 | M | `EN-PROGRESO` | | [`GT-580`](./gap-reference-catalog.es.md#gt-580) | `grep -rno "process.exit([0-9]*)" src/sdk/cli/src` devuelve **20 × `process.exit(1)` más 2 `process.exit()` sin argumento** — un único valor de fallo para un veredicto FAIL, un flag mal escrito, un fichero ausente o una caída de infraestructura, así que ningún consumidor puede ramificar por causa. Los diagnósticos comparten el canal máquina: **341 `console.log` frente a 115 `console.error`**. Y no hay salida incremental — no existe `--format ndjson` — así que un `validate` largo es opaco hasta que termina. Un harness de agente, un hook de pre-commit y un paso de CI tienen exactamente un primitivo en común (el código de salida del proceso) y el CLI hoy renuncia a usarlo. Fix: taxonomía publicada (`0` PASS · `2` error de uso · `3` **veredicto FAIL** · `1` fallo de infraestructura · `4` HITL requerido), todo diagnóstico a stderr, un stream de eventos NDJSON versionado, y la taxonomía gobernada por un ruleset propio con paridad Rego para que un comando nuevo no pueda romperla. **+ola 2026-07-28 — la taxonomía existe y su rotura de contrato está propagada.** `0` pasa · `1` fallo de herramienta · `2` bloqueado · `3` entrada inválida, impuesto centralmente en `BaseEvolithCommand.handleError` más un guard jest que rompe la build ante cualquier literal de salida fuera del conjunto. **La propagación se auditó en vez de suponerse:** un barrido de todo el repo no encontró NADA que interpretara `1` como "gate falló", así que no había rotura viva aquí — pero la composite action colapsaba todo lo no-cero en `non-compliant`, lo que **afirmaba que el repositorio de un cliente era no conforme cuando el CLI simplemente había reventado o se le había invocado mal**. Eso es el inverso de un gate que reporta verde sobre un repo sin evaluar, y es igual de falso. La action ahora reporta `error` / `invalid-input`, expone `exit-code`, falla el paso ante un no-veredicto ignorando `fail-on-violation`, y su resumen de PR dice "Not evaluated — esto no es un hallazgo sobre este repositorio". Sus tests pasaron de 11 a 13, reclasificados y no aflojados: cuatro casos eran veredictos bloqueados (ahora exit 2) y dos eran fallos de herramienta genuinos (siguen en exit 1, ahora `error`). Ambas guías publican la tabla de cuatro valores. **NO cerrado:** la disciplina stdout/stderr está intacta (341 `console.log` frente a 115 `console.error`), no hay flujo de eventos `--format ndjson`, y el tercer criterio pide un ruleset de paridad Rego mientras lo que aterrizó es un guard jest que sólo cubre este paquete. **Follow-up fuera de este repositorio:** el Tracker invoca el CLI y puede ramificar por su código de salida — no verificable desde aquí, hay que revisarlo en `evolith_tracker`. **Criterio 1 marcado el 2026-07-28 tras verificarlo:** `exit-code-taxonomy.spec.ts` publica exactamente cuatro códigos, separa un veredicto bloqueante de un Core inalcanzable y — la parte que sostiene el criterio — BARRE las fuentes de los comandos y comprueba que ninguna nombra un exit code fuera de `0`, `1`, `2` o `3`, con protección anti-vacua para que un verde sobre un barrido vacío no cuente como verde. 21/21. Quedan los criterios 2 y 3: no hay test que afirme que `--format json`/`ndjson` escribe datos solo en stdout con todo diagnóstico en stderr, ni un par ruleset+Rego que exija la taxonomía como gobierno en vez de como barrido de jest. **+ola 2026-07-29 — criterios 2 y 3 cerrados para el canal JSON; un barrido reforzado encontró cuatro roturas más, miembros de la taxonomía pero incorrectas.** Criterio 2: `machine-channel.registry-sweep.spec.ts` le pregunta al propio registro de commander del CLI qué comandos declaran `--format` (no una lista a mano) y comprueba que los 32 encontrados escriben exactamente un documento JSON parseable en stdout con los diagnósticos en stderr — 32/32 limpios. Esto cierra la mitad de pureza stdout/stderr del criterio 2; el streaming `--format ndjson`, el cuarto pilar del fix original, sigue sin construirse. Criterio 3: la taxonomía ahora está gobernada por `src/rulesets/cli/exit-code-taxonomy.rules.json` con paridad Rego (`src/rulesets/opa/cli-exit-code-taxonomy.rego`, `opa test` 7/7) y un evaluador nativo (`cli-exit-taxonomy-rule.handler.ts`, 28/28 junto con `rule-corpus-triage.spec.ts`) — un comando nuevo ya no puede romper la taxonomía sin que falle un ruleset. **Reforzar el barrido sacó a la luz justo la clase de bug para la que existe:** al sumar `envelope.success === (status === 0)` junto a la pertenencia a la taxonomía aparecieron CUATRO comandos que eran miembros válidos de la taxonomía mientras contradecían su propio envelope — `sdlc gate-status` salía con 0 sobre un envelope `INTERNAL_ERROR` (un consumidor que ramifica por código lo lee como PASS), `alias` salía con 1 en vez de 3 ante un error de uso, el catch-all de `init-wizard` colapsaba un `NonInteractiveError` a `INTERNAL_ERROR`/1 antes de que llegara a `resolveExitCode`, y la rama JSON de `sdlc generate` hacía `return` antes de llegar a la asignación de `process.exitCode` que tenía debajo. Los cuatro se corrigieron enrutando por los helpers ya existentes `exitCodeForErrorCode`/`resolveExitCode`/`carriesCliExitCode` en vez de un literal fijo, y el barrido ahora exige esa coherencia para todo comando de aquí en más. Verificado: suite del CLI 101 suites/1446 tests, handler+triage de core-domain 28/28, `opa test` 7/7, `tsc --noEmit` limpio. [PR #278](https://github.com/beyondnetcode/evolith_arch32/pull/278). **Sigue abierto:** el flujo de eventos `--format ndjson`, y el seguimiento cross-repo del Tracker señalado arriba (no verificable desde este repo). **Cierre 2026-07-31 (#305).** El canal máquina queda probado sobre todo el registry y la taxonomía de exit codes, gobernada. | `Evolith CLI` | Cross | P1 | M | `COMPLETADO` | | [`GT-581`](./gap-reference-catalog.es.md#gt-581) | `grep -rn "outputSchema\\|structuredContent" src/packages/mcp-server/src` devuelve **0**, e igual `annotations` — sobre **50 tools anunciadas**, con el SDK `1.29.0`, que soporta las tres cosas. Todo llamante recibe por tanto un bloque de texto y debe reconstruir su forma por ingeniería inversa, y ningún cliente puede distinguir una tool de solo lectura (`evolith-adr-list`) de una destructiva (`evolith-satellite-create`) antes de invocarla. El draft de la especificación además relajó `inputSchema`/`outputSchema` para aceptar cualquier keyword de JSON Schema 2020-12 (SEP-2106) y pide a los servidores devolver `tools/list` en orden determinista para mejorar el acierto de caché de cliente y de prompt — dos ganancias gratis en la misma pasada. Fix: derivar `outputSchema` por tool desde `@beyondnet/evolith-contracts`, emitir `structuredContent`, añadir annotations `readOnlyHint`/`destructiveHint`/`idempotentHint`, y hacer determinista el orden de `tools/list`. Ola 2: las 50 herramientas declaran `outputSchema` y devuelven `structuredContent` que valida con el propio validador del SDK — incluso en las denegaciones FORBIDDEN, que es el resultado que un agente más necesita leer mecánicamente. Se genera de forma central desde la versión del sobre, así que no se puede registrar una herramienta sin contrato. **Salvedad que se arrastra, no se oculta:** la rama `data` queda declarada-pero-abierta en las 50; estrecharla exige esquemas por operación, que es GT-583. | `MCP Server` | Cross | P1 | M | `COMPLETADO` | | [`GT-582`](./gap-reference-catalog.es.md#gt-582) | Verificado directamente contra la especificación viva el 2026-07-26, no tomado de una fuente secundaria. La revisión **actual** del protocolo es `2025-11-25`; el **draft** elimina las sesiones a nivel de protocolo y la cabecera `Mcp-Session-Id` (SEP-2567), elimina el handshake `initialize`/`notifications/initialized` en favor de `_meta` por petición (SEP-2575), hace obligatorio `server/discover` (SEP-2575), sustituye las peticiones iniciadas por el servidor con el patrón **MRTR** — `InputRequiredResult`, un `resultType` obligatorio, e `inputResponses` en un reintento de la petición original (SEP-2322) —, deprecia Roots/Sampling/Logging (SEP-2577) y deprecia el Dynamic Client Registration en favor de Client ID Metadata Documents. En este repositorio: SDK `1.29.0`, **7 sitios con `sessionId`** bajo `src/packages/mcp-server/src`, y `grep -rn "well-known\\|oauth-protected-resource" src` devuelve 2 coincidencias no relacionadas, así que tampoco se sirve documento de metadatos de recurso protegido. MRTR importa más allá de la conformidad: es *la aprobación como protocolo*, y es la única forma en que el gate HITL sobrevive a la desaparición de las sesiones. **La "corrección" anterior registrada aquí quedó a su vez refutada, y se reemplaza en vez de borrarse:** esta fila decía antes que la etiqueta `2026-07-28` "no pudo confirmarse" y que la página de versionado "sigue nombrando `2025-11-25` como actual". Verificado contra la especificación viva el 2026-07-31: **`2026-07-28` ES la revisión actual.** La página de versionado lo dice con esas palabras, describe la negociación como una clave `io.modelcontextprotocol/protocolVersion` por request dentro de `_meta` en vez de durante `initialize`, vuelve `server/discover` un RPC obligatorio, y remite a los clientes de la era del handshake (`2025-11-25` y anteriores) a una sección de compatibilidad hacia atrás. El contenido técnico estaba bien; la duda sobre la fecha estaba mal, y estaba mal en la dirección que habría diferido el trabajo. **CERRADO el 2026-07-31 en los tres criterios** — `protocol-revisions.ts` exporta ambas revisiones y `stateless-rpc.ts` las sirve en paralelo, MRTR es real (`mrtr-request-state.ts`, `requestState` sellado, reintento sin sesión), y se sirven los metadatos de recurso protegido RFC 9728 en `/.well-known/oauth-protected-resource` con puntero `WWW-Authenticate`. | `MCP Server` | Cross | P1 | L | `COMPLETADO` | diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index c102b8581..7968921ef 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -77,7 +77,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-575`](./gap-reference-catalog.md#gt-575) | **A product that sells AI governance ships its only LLM egress path without any of the controls it sells.** `GeminiProvider.ts:17` builds the URL with the API key in the query string; the whole 57-line file has no `AbortSignal` (`:31-37`), no budget, no redaction, no log or metric, and its only output validation is `JSON.parse(candidate) as T` (`:52`). It is a public export (`src/packages/agent-runtime/src/index.ts:22`) of `@beyondnet/evolith-agent-runtime@1.1.0`. Disclosure across `README.md`, `README.es.md`, `SECURITY.md` and the 8 package READMEs: zero. It violates at least 4 of the 9 blocking `AAI-*` rules the product itself sells. **Exposure is latent, not active** — the only in-tree caller is `src/sdk/cli/src/commands/plan/index.ts:27`, and `PlanCommand` is not registered in `app.module.ts` — but it sits on the public surface a security reviewer reads first. The correct implementation already exists in-house: `.harness/scripts/ci/agentic/review-provider.mjs:35-38` puts the key in a header with the literal comment "API key in a header, not the URL query string", with budget caps and 8 redaction patterns. Fix: port that control, and collapse the duplicate `ILLMProvider` port into the governed `IAssistantTransport`/`SupervisedAssistantClient` seam. **Progress (`44fe8dd3`):** the controls the repository already runs against itself in `.harness/scripts/ci/agentic/` were ported into the shipped provider rather than reinvented — API key moved from the URL query string to the `x-goog-api-key` header, `AbortController` timeout, byte/token budget that fails closed, secret redaction, schema-validated output, and an auditable log/metric. Egress is now **off unless `EVOLITH_LLM_EGRESS=true`**. **NOT closed:** turning an always-on published export into an opt-in one is a behavioural break for `@beyondnet/evolith-agent-runtime@1.1.0` (names stay additive, so the GT-388 contract freeze holds, but observable behaviour changed) and needs a deliberate version decision alongside `GT-570`; the duplicate `ILLMProvider` port is not yet fully collapsed into the governed `IAssistantTransport`/`SupervisedAssistantClient` seam; and the Spanish package READMEs still carry no disclosure. Verified: agent-runtime 309/309. **+2026-07-26 (wave 3) — 2 of 3 criteria met.** The duplicate LLM port was collapsed: `GeminiProvider` is now reachable only as an `IAssistantTransport` behind the supervised, fail-closed, off-by-default seam, so there is one governed path to an LLM instead of two with opposite postures. The Spanish egress disclosure was written into `src/packages/agent-runtime/README.es.md`, structurally mirroring the English one. **Still open:** criterion 3 — the repository does not pass its own 9 blocking `AAI-*` rules in a CI check, and there is still no root `agent.config.json`, so the agentic-ai topology is never evaluated against Evolith itself. Also unresolved and owner-gated: turning an always-on published export into an opt-in one is a behavioural break for `@beyondnet/evolith-agent-runtime@1.1.0` and needs a version decision alongside `GT-570`. `src/sdk/cli/README.es.md` still carries no disclosure (out of the agent's owned area). **CLOSED 2026-07-28, with the caveat that matters recorded rather than buried.** All three criteria are met: the key travels in a header with an `AbortController` timeout, an egress budget and secret redaction (`GeminiProvider`); the README carries a `Network egress and data handling` section naming the endpoint, what is sent, the opt-in and the sub-processor; and `agentic-ai-self-conformance.spec.ts` evaluates the repository against the nine blocking `AAI-*` rules it ships, 13/13, with a NEGATIVE FIXTURE proving all nine fail without `agent.config.json`. **The caveat:** that spec runs in `Test agent-runtime`, which is NOT one of main's seven required contexts (verified against the live branch protection). So the product now runs its own AI-egress rules against itself in CI — but a failure would not block a merge. Adding `Test agent-runtime` to the required set is a branch-protection change and an owner action; it is not something to do unilaterally, and it is the difference between a check that reports and a check that governs. | `agent-runtime` | Cross | P0 | S | `DONE` | | [`GT-576`](./gap-reference-catalog.md#gt-576) | **The self-assessment a buyer reads first is falsifiable in ten minutes, and the document incriminates itself.** `maturity-assessment.md` defines *Validated (Weight 1.0) — Passing all quality gates, tests, and active in CI/CD*, then marks **Pillar 1 Security "Level 4 (Managed) / Validated"** citing multi-tenant Row-Level Security (ADR-0010) and immutable audit trails via CDC (ADR-0016): `grep -rniE 'row.level.security\|current_setting\(\|debezium\|change data capture'` over `src` returns **ZERO files**, and core-api declares no database driver or ORM. **Pillar 4** is marked Level 4 / Validated citing "deterministic monorepo builds via Nx" — no `nx.json` and no `nx` dependency exist. It also claims dual-engine 8/8 parity while the gate covers 3 topologies and the published package ships policies for 5. (Pillar 3's stale `opossum` citation is a different matter — that pillar is honestly marked `Designed`.) Fix: downgrade Pillar 1 to `Designed` with the ADRs listed as intent, delete the Nx citation, report parity against the published artifact, and add a mechanical rule to `09-reconcile-maturity.mjs`: a capability may only be marked `Validated` if its evidence list contains at least one `file:line` reference or CI job, never an ADR alone. **Closure (`44fe8dd3`):** every claim in the statement was re-verified against the tree and all held. Pillar 1 (Security) and Pillar 4 dropped from `Validated` to `Designed` with their ADRs listed as intent, the Nx citation deleted, and parity reported against the published artifact — in BOTH languages, structurally identical. The class of error is now mechanically blocked: `09-reconcile-maturity.mjs` rejects any `Validated` capability whose evidence contains no `file:line` and no CI job, and that rule ships with a negative self-test that proves it goes red on a deliberately bad input. Verified: `09-reconcile-maturity.mjs --check` ✅, `04-check-bilingual-parity.mjs` ✅, governance 17/17. | `Governance` | Cross | P1 | S | `DONE` | | [`GT-577`](./gap-reference-catalog.md#gt-577) | **The artifact a customer would wire into their CI reads as a broken tool even though the gate works.** `.github/actions/evolith-validate/action.yml` reads `jq -r '.summary.violations // 0'`, but the real CLI envelope has top-level keys `[success, data, meta]` with `data = {status, rulesChecked, issues, coreRef, timestamp}` — there is no `.summary`. The same applies to the `--output` file (`validate.command.ts:353-361` writes the same `createSuccessEnvelope`). **Important nuance: the action does block correctly** — it captures `EXIT_CODE`, sets `compliance-status=non-compliant` and exits 1 when `fail-on-violation=true`; what is broken is the counter and the PR summary text, which renders literally "Non-compliant -- 0 violation(s) found". And `grep -rn 'evolith-validate' .github/` matches only inside the action's own README: zero consumers across the 12 workflows, so no regression is possible. Fix: change the jq path to `.data.issues \| map(select(.blocking)) \| length` and add a workflow in this repository that runs the action against a non-conforming satellite fixture, so it is dogfooded and regression-tested. **Progress (`44fe8dd3`):** the `jq` path now counts blocking issues from the real ADR-0073 envelope instead of reading a `.summary` key that never existed, and `.github/workflows/evolith-validate-dogfood.yml` runs the action against a deliberately non-conforming satellite fixture so the action is finally dogfooded. Validated by YAML parse and by executing the action's step scripts locally against the built CLI. **NOT closed:** the workflow has never executed on a GitHub runner, so by this audit's own standard the gate is not yet proven — a gate that has never been seen failing is the defect this whole wave is about. Close it after its first green run. Separately, it is not in the required-checks set, which is the `GT-574` owner action. **Closure (verified against CI run 30228607519, `develop`, 2026-07-27T00:56:37Z):** both jobs green — `Action contract (recorded envelopes)` and `Action vs non-conforming satellite`. The closure is NOT "the workflow is green": the log carries the two assertions that were impossible before, `OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.` and `OK: the gate blocked as expected with 34 blocking violation(s).` So the counter is non-zero, it agrees with the report file the action writes, `compliance-status` is `non-compliant`, and the negative half — `fail-on-violation: true` MUST fail the step — is exercised on every run. Five consecutive green runs. The action is now dogfooded and under regression, which was the more important half of this gap. Remaining and tracked elsewhere: the workflow is not in the required-checks set, which is branch-protection configuration. | `Infra` | Cross | P2 | XS | `DONE` | -| [`GT-578`](./gap-reference-catalog.md#gt-578) | **The two systemic root causes behind most findings of the 2026-07-26 audit, addressed at the mechanism rather than instance by instance.** (a) *Path literals*: the move to `src/` migrated code and imports but not the hundreds of path strings in CI scripts, workflow `run:` steps, evaluator constants and Helm values — the compiler catches a moved module, nothing catches a moved file referenced by a string, and a path that does not resolve produces silence. Live instances: `OpaEvaluator` hardcoding `/rulesets/opa/policy.wasm` (the whole OPA engine is non-functional against the Core layout); `upgrade` diffing against a nonexistent `/rulesets`; the CLI boundary config guarding `src/domain`, `src/application`, `src/core`, none of which has ever existed; `sdk-cli-ci.yml:467` invoking a nonexistent script whose real counterpart points at a pre-refactor path, so "Winston Agentic Review" is dead twice and reports `success`. (b) *Anti-vacuous pass*: the pattern already exists at `34-boundary-guard-repository.mjs:57-73` ("A zero-file scan must never be reported as boundary guard passed") and as a negative self-test in the Tracker contract gate — it is applied to 2 guards of ~46. Fix: a ~40-line guard resolving every path literal against disk; every guard publishes its denominator and exits 1 on a zero-element scan; every guard ships a deliberately bad fixture that MUST turn it red in CI; and CI executes every `validationCommand` in `gap-closure-evidence.json`. **Closure (`44fe8dd3`):** `40-validate-path-literals.mjs` scans harness scripts, workflow `run:` steps and Helm values, resolves each literal against disk, publishes its denominator (109 literals across 197 files) and exits 1 on a vacuous scan — it obeys the discipline it enforces, with a negative fixture in `node --test` (15 tests). Both live defects it found are repaired: `sdk-cli-ci.yml:467` invoked a nonexistent `.harness/scripts/ci/13-agentic-code-review.mjs` (real path under `ci/agentic/`) and that script's argv targeted a pre-refactor `packages/mcp-server/dist/main.js` — which is why "Winston Agentic Review" reported `success` for months while never reaching the server. With both fixed, the guard was flipped from `--report` to its default STRICT mode in `ci-runner.mjs`, so a new dead literal now FAILS the governance run. The anti-vacuous half is enforced for this guard; extending the pattern to the other ~45 guards remains follow-on work. **NOT closed:** two of its three criteria remain unmet — the anti-vacuous-pass pattern is enforced for THIS guard only (~45 others to go), and the board's `validationCommands` are still not executed in CI. The path-literal guard itself is strict, wired and green. **+2026-07-26 (wave 3) — two new guards landed; criteria 2 and 3 advanced, neither closed.** `42-validate-guard-denominators.mjs` classifies all **54** CI guards and asserts none can pass a zero-element scan silently (8/8 unit tests, exit 0). `41-validate-evidence-commands.mjs` finally EXECUTES the board's `validationCommands` instead of only checking they are non-empty strings (27/27 unit tests), and it reports its denominator honestly: over **937 recorded commands across 544 closure records it finds 290 dead references**, and it prints `THIS IS NOT A PASS` rather than exiting green on a number that large. It is wired in report mode with a `--max-dead 290` ratchet so the count can only go down. **Still open:** criterion 3 needs dead == 0, which is a data migration of the board-owned evidence file, not a guard change; criterion 2 needs a negative fixture per guard, which classification does not substitute for. The 290 dead references are themselves a finding: they are the measured size of root cause C5 — a board that records intent rather than result. Wave 2: criterion 2 is now enforced by EXECUTION — `43-validate-guard-negative-fixtures.mjs` points every scanning guard at one deliberately empty tree and 32/32 turned red, 0 reported a pass. One shared fixture rather than 34 bespoke ones, derived from 42’s classification, so the next guard is exercised the day it lands. Two false-DEAD classes in 41 repaired and the ratchet made machine-independent (it now prints its own basis: 303, agreed between a laptop and the runner despite raw counts differing by 15). 43, 44 and 34 wired into ci-cd.yml, ratchet lowered 305→303. **AC3 untouched:** 288 dead references, 198 naming pre-refactor workspaces — a data migration of the evidence file. | `Governance` | Cross | P1 | M | `IN-PROGRESS` | +| [`GT-578`](./gap-reference-catalog.md#gt-578) | **The two systemic root causes behind most findings of the 2026-07-26 audit, addressed at the mechanism rather than instance by instance.** (a) *Path literals*: the move to `src/` migrated code and imports but not the hundreds of path strings in CI scripts, workflow `run:` steps, evaluator constants and Helm values — the compiler catches a moved module, nothing catches a moved file referenced by a string, and a path that does not resolve produces silence. Live instances: `OpaEvaluator` hardcoding `/rulesets/opa/policy.wasm` (the whole OPA engine is non-functional against the Core layout); `upgrade` diffing against a nonexistent `/rulesets`; the CLI boundary config guarding `src/domain`, `src/application`, `src/core`, none of which has ever existed; `sdk-cli-ci.yml:467` invoking a nonexistent script whose real counterpart points at a pre-refactor path, so "Winston Agentic Review" is dead twice and reports `success`. (b) *Anti-vacuous pass*: the pattern already exists at `34-boundary-guard-repository.mjs:57-73` ("A zero-file scan must never be reported as boundary guard passed") and as a negative self-test in the Tracker contract gate — it is applied to 2 guards of ~46. Fix: a ~40-line guard resolving every path literal against disk; every guard publishes its denominator and exits 1 on a zero-element scan; every guard ships a deliberately bad fixture that MUST turn it red in CI; and CI executes every `validationCommand` in `gap-closure-evidence.json`. **Closure (`44fe8dd3`):** `40-validate-path-literals.mjs` scans harness scripts, workflow `run:` steps and Helm values, resolves each literal against disk, publishes its denominator (109 literals across 197 files) and exits 1 on a vacuous scan — it obeys the discipline it enforces, with a negative fixture in `node --test` (15 tests). Both live defects it found are repaired: `sdk-cli-ci.yml:467` invoked a nonexistent `.harness/scripts/ci/13-agentic-code-review.mjs` (real path under `ci/agentic/`) and that script's argv targeted a pre-refactor `packages/mcp-server/dist/main.js` — which is why "Winston Agentic Review" reported `success` for months while never reaching the server. With both fixed, the guard was flipped from `--report` to its default STRICT mode in `ci-runner.mjs`, so a new dead literal now FAILS the governance run. The anti-vacuous half is enforced for this guard; extending the pattern to the other ~45 guards remains follow-on work. **NOT closed:** two of its three criteria remain unmet — the anti-vacuous-pass pattern is enforced for THIS guard only (~45 others to go), and the board's `validationCommands` are still not executed in CI. The path-literal guard itself is strict, wired and green. **+2026-07-26 (wave 3) — two new guards landed; criteria 2 and 3 advanced, neither closed.** `42-validate-guard-denominators.mjs` classifies all **54** CI guards and asserts none can pass a zero-element scan silently (8/8 unit tests, exit 0). `41-validate-evidence-commands.mjs` finally EXECUTES the board's `validationCommands` instead of only checking they are non-empty strings (27/27 unit tests), and it reports its denominator honestly: over **937 recorded commands across 544 closure records it finds 290 dead references**, and it prints `THIS IS NOT A PASS` rather than exiting green on a number that large. It is wired in report mode with a `--max-dead 290` ratchet so the count can only go down. **Still open:** criterion 3 needs dead == 0, which is a data migration of the board-owned evidence file, not a guard change; criterion 2 needs a negative fixture per guard, which classification does not substitute for. The 290 dead references are themselves a finding: they are the measured size of root cause C5 — a board that records intent rather than result. Wave 2: criterion 2 is now enforced by EXECUTION — `43-validate-guard-negative-fixtures.mjs` points every scanning guard at one deliberately empty tree and 32/32 turned red, 0 reported a pass. One shared fixture rather than 34 bespoke ones, derived from 42’s classification, so the next guard is exercised the day it lands. Two false-DEAD classes in 41 repaired and the ratchet made machine-independent (it now prints its own basis: 303, agreed between a laptop and the runner despite raw counts differing by 15). 43, 44 and 34 wired into ci-cd.yml, ratchet lowered 305→303. **2026-08-01 follow-up:** the data migration advanced the evidence file to **0 local dead references** and a CI clean-checkout basis of **17** generated/gitignored referents, so the ratchet moved to `--max-dead 17`. AC3 remains open because the full executable sweep now has evidence instead of a blind pass: `--execute --strict --max-dead 17` ran 109 unique candidates, 104 green, 2 non-zero (`GT-42`, `GT-280`) and 3 search-with-no-match outcomes. | `Governance` | Cross | P1 | M | `IN-PROGRESS` | | [`GT-580`](./gap-reference-catalog.md#gt-580) | `grep -rno "process.exit([0-9]*)" src/sdk/cli/src` returns **20 × `process.exit(1)` plus 2 bare `process.exit()`** — one single failure value for a FAIL verdict, a bad flag, a missing file and an infra crash alike, so no consumer can branch on the cause. Diagnostics share the machine channel: **341 `console.log` against 115 `console.error`**. And there is no incremental output — no `--format ndjson` — so a long `validate` is opaque until it terminates. An agent harness, a pre-commit hook and a CI step all have exactly one primitive in common (the process exit code) and the CLI currently declines to use it. Fix: a published taxonomy (`0` PASS · `2` usage error · `3` **verdict FAIL** · `1` infra failure · `4` HITL required), every diagnostic to stderr, a versioned NDJSON event stream, and the taxonomy governed by its own ruleset with Rego parity so a new command cannot regress it. **+wave 2026-07-28 — the taxonomy exists and its contract break is propagated.** `0` pass · `1` tool failure · `2` blocked · `3` invalid input, enforced centrally in `BaseEvolithCommand.handleError` plus a jest guard that fails the build on any exit literal outside the set. **The propagation was audited rather than assumed:** a repo-wide sweep found NOTHING branching on `1` as "gate failed", so there was no live break here — but the composite action collapsed every non-zero onto `non-compliant`, which **asserted a customer's repository was non-compliant when the CLI had merely crashed or been invoked wrongly**. That is the inverse of a gate reporting green over an unevaluated repo, and it is just as false. The action now reports `error` / `invalid-input`, exposes `exit-code`, fails the step for a non-verdict regardless of `fail-on-violation`, and its PR summary says "Not evaluated — this is not a finding about this repository". Its tests went 11 → 13, reclassified rather than loosened: four cases were blocked verdicts (now exit 2) and two were genuine tool failures (still exit 1, now `error`). Both language guides publish the four-value table. **NOT closed:** stdout/stderr discipline is untouched (341 `console.log` vs 115 `console.error`), there is no `--format ndjson` event stream, and the third criterion asks for a Rego-parity ruleset while what shipped is a jest guard covering only this package. **Follow-up outside this repository:** the Tracker invokes the CLI and may branch on its exit code — unverifiable from here and must be checked in `evolith_tracker`. **Criterion 1 ticked on 2026-07-28 after verification:** `exit-code-taxonomy.spec.ts` publishes exactly four codes, separates a blocked verdict from an unreachable Core, and — the load-bearing part — SCANS the command sources and asserts none names an exit code outside `0`, `1`, `2` or `3`, with an anti-vacuous guard so a green from an empty scan is not a green. 21/21. Criteria 2 and 3 remain: there is no test asserting that `--format json`/`ndjson` writes data only to stdout with every diagnostic on stderr, and no ruleset+Rego pair enforcing the taxonomy as governance rather than as a jest scan. **+wave 2026-07-29 — criteria 2 and 3 closed for the JSON channel; a strengthened sweep then found four more taxonomy-membership-but-wrong breaks.** Criterion 2: `machine-channel.registry-sweep.spec.ts` asks the CLI's own commander registry which of its commands declare `--format` (not a hand-picked list) and asserts every one of the 32 found writes exactly one parseable JSON document to stdout with diagnostics on stderr — 32/32 clean. This closes the stdout/stderr purity half of criterion 2; `--format ndjson` streaming, the fourth pillar of the original fix, is still unbuilt. Criterion 3: the taxonomy is now governed by `src/rulesets/cli/exit-code-taxonomy.rules.json` with Rego parity (`src/rulesets/opa/cli-exit-code-taxonomy.rego`, `opa test` 7/7) and a native evaluator (`cli-exit-taxonomy-rule.handler.ts`, 28/28 with `rule-corpus-triage.spec.ts`) — a new command can no longer regress the taxonomy without a ruleset failure. **Strengthening the sweep surfaced exactly the class of bug it exists to catch:** adding `envelope.success === (status === 0)` alongside taxonomy-membership found FOUR commands that were valid taxonomy members while contradicting their own envelope — `sdlc gate-status` exited 0 on an `INTERNAL_ERROR` envelope (a branching consumer reads that as a PASS), `alias` exited 1 instead of 3 on a usage error, `init-wizard`'s own catch-all collapsed a `NonInteractiveError` to `INTERNAL_ERROR`/1 before it ever reached `resolveExitCode`, and `sdlc generate`'s JSON branch `return`ed before reaching the `process.exitCode` assignment below it. All four fixed by routing through the existing `exitCodeForErrorCode`/`resolveExitCode`/`carriesCliExitCode` helpers rather than a hardcoded literal, and the sweep now asserts this agreement for every command going forward. Verified: CLI suite 101 suites/1446 tests, core-domain handler+triage 28/28, `opa test` 7/7, `tsc --noEmit` clean. [PR #278](https://github.com/beyondnetcode/evolith_arch32/pull/278). **Still open:** the `--format ndjson` event stream, and the Tracker cross-repo follow-up noted above (unverifiable from this repo). **Closure 2026-07-31 (#305).** The machine channel is proven across the whole registry and the exit-code taxonomy is governed. | `Evolith CLI` | Cross | P1 | M | `DONE` | | [`GT-581`](./gap-reference-catalog.md#gt-581) | `grep -rn "outputSchema\\|structuredContent" src/packages/mcp-server/src` returns **0**, and so does `annotations` — across **50 announced tools**, on SDK `1.29.0`, which supports all three. Every caller therefore receives a text block and must reverse-engineer its shape, and no client can tell a read-only tool (`evolith-adr-list`) from a destructive one (`evolith-satellite-create`) before invoking it. The specification draft additionally loosened `inputSchema`/`outputSchema` to accept any JSON Schema 2020-12 keyword (SEP-2106) and asks servers to return `tools/list` in a deterministic order to improve client and prompt-cache hit rates — both free wins on the same pass. Fix: derive `outputSchema` per tool from `@beyondnet/evolith-contracts`, emit `structuredContent`, add `readOnlyHint`/`destructiveHint`/`idempotentHint` annotations, and make `tools/list` ordering deterministic. Wave 2: all 50 tools declare an outputSchema and return structuredContent that validates under the SDK’s own validator — including on FORBIDDEN denials, the result an agent most needs to read mechanically. Generated centrally from the envelope version, so a new tool cannot be registered without a contract. **Caveat carried, not hidden:** the `data` branch is declared-but-open for all 50; narrowing it needs per-operation schemas, which is GT-583. | `MCP Server` | Cross | P1 | M | `DONE` | | [`GT-582`](./gap-reference-catalog.md#gt-582) | Verified directly against the live specification on 2026-07-26, not taken from a secondary source. The **current** protocol revision is `2025-11-25`; the **draft** removes protocol-level sessions and the `Mcp-Session-Id` header (SEP-2567), removes the `initialize`/`notifications/initialized` handshake in favour of per-request `_meta` (SEP-2575), makes `server/discover` mandatory (SEP-2575), replaces server-initiated requests with the **MRTR** pattern — `InputRequiredResult`, a required `resultType`, and `inputResponses` on a retry of the original request (SEP-2322) — deprecates Roots/Sampling/Logging (SEP-2577) and deprecates Dynamic Client Registration in favour of Client ID Metadata Documents. In this repository: SDK `1.29.0`, **7 `sessionId` sites** under `src/packages/mcp-server/src`, and `grep -rn "well-known\\|oauth-protected-resource" src` returns 2 unrelated matches, so no protected-resource metadata document is served either. MRTR matters beyond conformance: it is *approval as a protocol*, and it is the only way the HITL gate survives the removal of sessions. **The earlier "correction" recorded here has itself been refuted, and is replaced rather than deleted:** this row previously said the `2026-07-28` label "could not be confirmed" and that the spec's versioning page "still names `2025-11-25` as current". Re-checked against the live specification on 2026-07-31: **`2026-07-28` IS the current revision.** The versioning page states it in those words, describes negotiation as a per-request `io.modelcontextprotocol/protocolVersion` key in `_meta` rather than during `initialize`, makes `server/discover` a mandatory RPC, and points handshake-era clients (`2025-11-25` and earlier) at a backward-compatibility section. The technical content was right; the doubt about the date was wrong, and it was wrong in the direction that would have deferred the work. **CLOSED 2026-07-31 on all three criteria** — `protocol-revisions.ts` exports both revisions and `stateless-rpc.ts` serves them side by side, MRTR is real (`mrtr-request-state.ts`, sealed `requestState`, retry without a session), and RFC 9728 protected-resource metadata is served at `/.well-known/oauth-protected-resource` with a `WWW-Authenticate` pointer. | `MCP Server` | Cross | P1 | L | `DONE` |