Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c171eb6
fix(c-009): add make cypher-lint and close live interpolation holes
cryptoxdog Sep 19, 2026
e9efff7
style: commit gate writer rewrites so make pr finishes once
cryptoxdog Sep 19, 2026
4a553b3
fix(c-009): return label-safety check as a boolean
cryptoxdog Sep 19, 2026
38ece3f
test(c-009): cover cypher_quoted_ident wrap and reject
cryptoxdog Sep 19, 2026
97b41d5
fix(ci): declare consumer repo_class python for org-ci language detect
cryptoxdog Sep 19, 2026
b30e638
fix(ceg): close CEG-001..009 from the Constellation E2E action log
claude Sep 19, 2026
7ecf4ec
fix(ceg): repair kernel-surfaced drift from the CEG-007 removal
claude Sep 19, 2026
e2075e7
fix(ceg): remediate PR #283 review findings and mypy CI failure
claude Sep 19, 2026
b499ee5
fix(ci): restore L9 analysis and settle Protocol body + Scorecard GHCR
cryptoxdog Sep 20, 2026
e2cd163
style: commit gate writer rewrites so make pr finishes once
cryptoxdog Sep 20, 2026
6790da7
Merge branch 'pr-281' into claude/audit-findings-fixes-93rt0t
claude Sep 20, 2026
a41943e
Merge branch 'pr-283' into claude/audit-findings-fixes-93rt0t
claude Sep 20, 2026
327a2a0
Merge branch 'pr-284' into claude/audit-findings-fixes-93rt0t
claude Sep 20, 2026
054ba11
fix(c-009): AST cypher-lint scanner and value parameterization (F280-…
claude Sep 20, 2026
ebd3c7e
fix(driver): make concurrent first-use callers await in-flight provis…
claude Sep 20, 2026
a130b35
ci: keep L9 analysis central; test: enforce docstring-only Protocol b…
claude Sep 20, 2026
dbf4ce1
fix(ci): validated numeric casts, logger docstring examples, no bare …
claude Sep 20, 2026
6013efa
feat(deps): detect a stale Gate_SDK lock, and enforce the pin in CI
claude Sep 20, 2026
80d97de
fix: address Codex and Copilot review on PR #285
claude Sep 20, 2026
a56ea12
docs(contracts): point the SDK install contract at the canonical channel
claude Sep 20, 2026
1712791
Merge #285: turn its accepted stale-lock risk into a detected failure
claude Sep 20, 2026
328fe43
fix(security): refuse a Gate_SDK remote git would parse as an option
claude Sep 20, 2026
ea2bdef
fix(security): the canonical remote is the contract, not a CLI option
claude Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/rules/capability-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ Before building any of these, check if it already exists. Use the existing imple
| Pareto scoring | engine/scoring/pareto.py | multi-objective optimization |
| Convergence loop | engine/feedback/convergence.py | `ConvergenceLoop` |
| Label sanitization | engine/utils/security.py | `sanitize_label()` |
| CEG → Gate → EIE egress (the ONLY peer egress) | engine/gate_egress.py | `request_enrichment()`, `emit_graph_inference_result()` |
| Inference outputs → EIE wire shape | engine/gate_egress.py | `build_inference_outputs()` (0.55 floor, matches EIE) |
| Domain database provisioning | engine/graph/driver.py | `GraphDriver.ensure_database()` (flag: `auto_create_domain_database`) |
| Database-name validation for DDL | engine/utils/security.py | `sanitize_database_name()` — `sanitize_label()` forbids the dashes domain ids use |
| Gate run-time values as `$params` | engine/gates/types/all_gates.py | `BaseGate._bind_param()` / `BaseGate.query_params` |
| Cypher injection scanner (C-009) | tools/cypher_lint.py | `scan_tree()` — AST f-string role classifier |
| Inference rule execution | engine/inference_rule_registry.py | `execute_rule()`, `list_registered_rules()` |

Do not add a second outbound client. `engine/gate_egress.py` is the only
CEG → peer egress; Gate resolves the destination from the action.
9 changes: 9 additions & 0 deletions .claude/rules/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ All in Settings class, controllable via env vars. Contract 21: every behavioral
| strict_tenant_database | False | Require explicit `database=` on GraphDriver calls; no implicit 'neo4j' fallback (W7-01) |
| require_sdk_chassis_in_prod | False | Fail startup if `L9_CHASSIS != sdk` when `l9_env == prod` (W7-02) |

## Constellation Seam (EIE ↔ Gate ↔ CEG)
| Flag | Default | Purpose |
|------|---------|---------|
| auto_enrich_via_gate | False | Dispatch `enrich` to EIE through Gate (spends EIE budget) |
| graph_inference_feedback_enabled | False | Emit `graph-inference-result` to EIE through Gate (EIE-008 / CEG-006) |
| auto_create_domain_database | False | Provision the tenant domain database on first use (CEG-008; Enterprise-only CREATE DATABASE) |
| health_api_enabled | False | Expose engine/health/api.py via admin health_* subactions (CEG-006) |
| unvalidated_domain_packs_enabled | False | Serve the 5 migrated packs whose gates do not compile to executable Cypher (CEG-009) |

## Entity Resolution
| Flag | Default | Purpose |
|------|---------|---------|
Expand Down
9 changes: 9 additions & 0 deletions .claude/rules/subsystems.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ All handlers: `async def handle_*(tenant: str, payload: dict) -> dict`
| calibration_run | Score calibration vs expected ranges |
| score_feedback | Compute weight adjustment proposal |
| apply_weight_proposal | Apply proposed weight change |
| health_assess | AI-readiness assessment for one entity (`engine/health/api.py`) |
| health_batch_assess | Incremental batch readiness scan with cost ceilings |
| health_report | Health report for an entity (Seed tier) |
| emit_inference_feedback | Run inference rules and send `graph-inference-result` to EIE via Gate (flag: `graph_inference_feedback_enabled`) |

CEG-006: the three `health_*` subactions are what makes `engine/health/api.py`
reachable. It was imported by nothing, so `trigger_reenrichment_v2` — and with
it the whole CEG → Gate → EIE enrichment request — had no trigger any inbound
packet could reach.

## Dependency Map
```
Expand Down
20 changes: 14 additions & 6 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=l9-dev-password
NEO4J_POOL_SIZE=20

── Redis Cache ─────────────────────────────────────────────
REDIS_URL=redis://redis:6379/0

── API Configuration ───────────────────────────────────────
API_KEY=dev-key-sha256-not-for-production
LOG_LEVEL=debug
Expand All @@ -47,7 +44,6 @@ LOCAL DEVELOPMENT ACCESS (from host machine, not Docker)
API: http://localhost:8000/v1/health
Neo4j Browser: http://localhost:7475
Neo4j Bolt: bolt://localhost:7688
Redis: localhost:6379
Neo4j Credentials:
User: neo4j
Password: l9-dev-password
Expand All @@ -68,11 +64,16 @@ KGE_EMBEDDING_DIM=300
GATE_URL=http://gate:9000
GATE_ADMIN_TOKEN=your-gate-admin-token-here
L9_NODE_NAME=graph
L9_NODE_SPEC_PATH=engine/spec.yaml
GATE_REGISTRATION_ENABLED=true
GATE_REGISTER_OVERWRITE=true
GATE_REGISTER_RETRIES=3
GATE_CLIENT_TIMEOUT_SECONDS=30.0
# CEG-003: this is the name the SDK reads (gate/config.py:
# spec_path=os.getenv("GATE_NODE_SPEC_PATH", "engine/spec.yaml")). An
# L9_NODE_SPEC_PATH line sat above it with no reader at all, so an operator
# relocating the spec edited the variable that does nothing and registration
# kept using the default until that path stopped existing — at which point
# register_with_gate swallows FileNotFoundError and returns False.
GATE_NODE_SPEC_PATH=engine/spec.yaml

# ── SDK chassis (L9_CHASSIS=sdk) ─────────────────────────────
Expand Down Expand Up @@ -104,7 +105,14 @@ L9_SIGNING_ALGORITHM=hmac-sha256
L9_SIGNING_KEY=change-me-shared-hmac-secret
L9_SIGNING_KEY_ID=graph-engine-v1
# ed25519 alternative: L9_SIGNING_ALGORITHM=ed25519 + L9_SIGNING_PRIVATE_KEY
# L9_VERIFYING_KEYS_JSON={"gate-v1":"<public-key>"}
#
# CEG-002: REQUIRED whenever L9_REQUIRE_SIGNATURE=true, not optional. The same
# flag turns on verification of the responses Gate signs, and Gate signs with
# *its* key id, which CEG cannot resolve from its own signing key. Leave this
# unset and every signed Gate response is rejected with "no verifying key
# available for transport signature verification". Under hmac-sha256 the value
# is the shared secret; under ed25519 it is the peer's public key.
L9_VERIFYING_KEYS_JSON={"gate-v1":"change-me-shared-hmac-secret"}

# Allowed actions — engine.handlers.ACTION_HANDLERS minus `enrich`: Gate owns
# the `enrich` name for EIE, so CEG must never accept or advertise it.
Expand Down
11 changes: 1 addition & 10 deletions .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,7 @@ jobs:
--health-timeout 5s
--health-retries 5

redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
# CEG-007: redis service container removed with the dependency.

steps:
- name: Checkout code
Expand All @@ -215,7 +207,6 @@ jobs:
- name: Run tests with coverage
env:
DATABASE_URL: postgresql://${{ vars.TEST_DB_USER || 'test' }}:${{ vars.TEST_DB_PASSWORD || 'test' }}@localhost:5432/${{ vars.TEST_DB_NAME || 'test_db' }}
REDIS_URL: redis://localhost:6379
TESTING: "true"
run: PYTHONPATH=. pytest tests/ -v --cov=engine --cov-report=xml --cov-report=term --ignore=tests/e2e

Expand Down
24 changes: 14 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ jobs:
python tools/check_deprecated_imports.py --check .
echo "✅ No deprecated DomainSpecLoader usage found"

- name: Gate_SDK declares the v1 channel (offline)
run: |
python scripts/validate_sdk_pin.py

- name: Gate_SDK lock agrees with the v1 channel (networked)
# The structural check above cannot see a stale lock: `v1` moves inside
# Gate_SDK and nothing here changes. This resolves the channel at the
# canonical remote and fails closed if poetry.lock no longer matches.
run: |
python scripts/validate_sdk_pin.py --verify-tag

# PacketEnvelope prohibition is governed by the baseline ratchet
# (.github/workflows/baseline-ratchet-caller.yml → l9-ci-core
# "Baseline Ratchet / Quarantined Debt", fail-closed). The l9-ci-sdk
Expand Down Expand Up @@ -258,15 +269,9 @@ jobs:
--health-retries 5
# Service only runs if POSTGRES_ENABLED=true (GitHub ignores services with falsy conditions)

redis:
image: ${{ vars.REDIS_IMAGE || 'redis:7-alpine' }}
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
# CEG-007: the redis service container is gone with the dependency. No
# code under engine/ or chassis/ imports redis, so it started a container
# every run for nothing.

steps:
- name: Checkout Repository
Expand All @@ -291,7 +296,6 @@ jobs:
- name: Run Tests with Coverage
env:
DATABASE_URL: postgresql://${{ vars.POSTGRES_USER || 'test_user' }}:test_password@localhost:5432/${{ vars.POSTGRES_DB || 'test_db' }}
REDIS_URL: redis://localhost:6379/0
run: |
echo "Running test suite..."
PYTHONPATH=. pytest ${{ env.TEST_DIR }} \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/supply-chain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ jobs:
persist-credentials: false

- name: Run OpenSSF Scorecard Analysis
# v2.4.4 action.yaml image is docker://ghcr.io/ossf/scorecard-action:v2.4.4
# Do not pin v2.4.0 — that tag still docker-pulls gcr.io/openssf (billing denied).
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: scorecard.sarif
Expand Down
4 changes: 4 additions & 0 deletions .l9/ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"schema": "l9.ci-consumer/v1",
"repo_class": "python"
}
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ repos:
fi; exit 0'
language: system
pass_filenames: false
- id: cypher-lint
name: make cypher-lint (C-009)
entry: python tools/cypher_lint.py
language: python
pass_filenames: false
types: [python]

# L9 contract enforcement (24 invariants, 27 docs)
- repo: local
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Cross-tool agent instructions for the CEG repository. Read by Claude Code, Codex

```bash
make setup # Install deps, pre-commit hooks, verify Neo4j
make dev # docker-compose (app + Neo4j + Redis + Prometheus + Grafana)
make dev # docker-compose (app + Neo4j + Postgres)
make test # Full pytest suite (unit + integration + compliance)
make test-unit # Gate compilation, scoring math, parameter resolution
make test-integration # testcontainers-neo4j full pipeline
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ All notable changes to L9 Engine will be documented in this file.
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Security (C-009)
- `tools/cypher_lint.py`: rewritten as an AST scanner that classifies every f-string
interpolation by syntactic role (quoted value, label, property, parameter name,
back-quoted identifier, `LIMIT`/`SKIP`, compiled fragment) instead of gating on a
keyword list; explicit reasoned waivers (`# cypher-lint: allow <reason>`) are
printed on every run.
- `engine/gates/compiler.py`, `engine/gates/types/all_gates.py`: query parameter
names are validated with `sanitize_label()`; operators and composite logic pass
through literal allow-lists; `EnumMapGate` mapping keys/values and GDS equipment
type names travel as `$parameters` (`BaseGate._bind_param()` / `query_params`).
- `engine/scoring/*`: dimension aliases validated, numeric spec values cast before
interpolation, `helpfulness`/`importance` builders validate property and
parameter names.
- `engine/utils/security.py`: `cypher_quoted_ident()` removed; `sanitize_database_name()`
added for `CREATE DATABASE`.

### Fixed
- `GraphDriver.ensure_database()`: concurrent first-use callers await the single
in-flight `CREATE DATABASE` instead of racing past a pre-claimed name into a
database that does not exist yet (CEG-008 follow-up).
- `tests/unit/test_protocol_bodies.py`: enforces the docstring-only Protocol body
contract — any statement after the docstring fails with its location.

### CI
- The consumer-local `l9-analysis.yml` workflow is not restored; organization
analysis stays owned by `l9-ci-core` (`Analyze (central Core)`).

## [1.2.0] - 2026-03-10 — KGE Mathematical Core Hardening

### Patches Applied
Expand Down
20 changes: 7 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# ─────────────────────────────────────────────────────────────

.PHONY: dev dev-build dev-down dev-logs dev-restart health
.PHONY: test test-unit test-integration seed shell neo4j-shell
.PHONY: test test-unit test-integration seed shell neo4j-shell cypher-lint

# ── Governance ─────────────────────────────────────────────

Expand Down Expand Up @@ -45,8 +45,6 @@ health:Check all service health
@curl -sf http://localhost:8000/v1/health | python -m json.tool || echo "API: DOWN"
@echo "── Neo4j ──"
@docker exec l9-graph-neo4j cypher-shell -u neo4j -p l9-dev-password "RETURN 'ok'" 2>/dev/null || echo "Neo4j: DOWN"
@echo "── Redis ──"
@docker exec l9-graph-redis redis-cli ping || echo "Redis: DOWN"

# ── Testing ────────────────────────────────────────────────

Expand All @@ -72,21 +70,17 @@ shell:Python shell inside API container
neo4j-shell:Cypher shell into Neo4j
docker exec -it l9-graph-neo4j cypher-shell -u neo4j -p l9-dev-password

redis-shell:Redis CLI
docker exec -it l9-graph-redis redis-cli

# ── Local Dev (API outside Docker, DBs in Docker) ─────────

local-dbs:Start only Neo4j + Redis
docker compose up -d neo4j redis
local-dbs:Start only Neo4j + Postgres
docker compose up -d neo4j postgres

local-api:Run API locally against Dockerized DBs (SDK chassis; alias of local-api-sdk)
$(MAKE) local-api-sdk

local-api-legacy:Run the legacy dict chassis locally (permitted in L9_ENV=dev|local|test)
PLASTICOS_NEO4J_URI=bolt://localhost:7687 \
PLASTICOS_NEO4J_PASSWORD=l9-dev-password \
PLASTICOS_REDIS_URL=redis://localhost:6379/0 \
PLASTICOS_LOG_LEVEL=debug \
L9_LIFECYCLE_HOOK=engine.boot:GraphLifecycle \
L9_CHASSIS=legacy \
Expand All @@ -95,7 +89,6 @@ local-api-legacy:Run the legacy dict chassis locally (permitted in L9_ENV=dev|lo
local-api-sdk:Run API locally on the SDK chassis (L9_CHASSIS=sdk)
PLASTICOS_NEO4J_URI=bolt://localhost:7687 \
PLASTICOS_NEO4J_PASSWORD=l9-dev-password \
PLASTICOS_REDIS_URL=redis://localhost:6379/0 \
PLASTICOS_LOG_LEVEL=debug \
L9_LIFECYCLE_HOOK=engine.boot:GraphLifecycle \
L9_CHASSIS=sdk \
Expand Down Expand Up @@ -199,8 +192,6 @@ deploy-health: guard-vps-host ## Remote healthcheck over SSH (VPS ports may be f
@ssh $(SSH_OPTS) $(SSH_TARGET) "curl -sf http://localhost:8000/v1/health && echo" || echo "API: DOWN"
@echo "── Neo4j ──"
@ssh $(SSH_OPTS) $(SSH_TARGET) "curl -sf http://localhost:7474 >/dev/null" && echo "Neo4j: UP" || echo "Neo4j: DOWN"
@echo "── Redis ──"
@ssh $(SSH_OPTS) $(SSH_TARGET) "docker exec l9-redis-prod redis-cli ping" || echo "Redis: DOWN"

# ── Cleanup ────────────────────────────────────────────────

Expand All @@ -209,13 +200,16 @@ clean: ## Remove volumes + containers

# ── Quality Gates (local, no Docker) ───────────────────────

.PHONY: lint lint-fix typecheck check
.PHONY: lint lint-fix typecheck check cypher-lint

lint: ## Ruff lint + format check (no mutation) + MyPy — matches CI's blocking gate
ruff check .
ruff format --check .
mypy engine/

cypher-lint: ## C-009: scan generated Cypher for injection vectors
python3 tools/cypher_lint.py

lint-fix: ## Autofix: ruff check --fix + ruff format . (run this when `make lint` fails)
ruff check . --fix
ruff format .
Expand Down
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ One engine. Any vertical. No custom code per domain.
git clone <repo-url> && cd l9-engine
./scripts/setup.sh

# 2. Start local stack (Neo4j + Redis + API)
# 2. Start local stack (Neo4j + Postgres + API)
./scripts/dev.sh

# 3. Seed sample data
Expand Down Expand Up @@ -81,8 +81,8 @@ All intelligence features are disabled by default and activated per-domain via Y
│ Domain Spec Loader │
│ YAML → Pydantic → Compiled Cypher │
├──────────────────────────────────────────────────────┤
│ Neo4j (multi-database) │ Redis (cache/scheduler)
└─────────────────────────────────────────────────────┘
Neo4j (multi-database)
└─────────────────────────────────────────────────────┘
```

### Core Concepts
Expand Down Expand Up @@ -121,8 +121,7 @@ l9-engine/
│ │ ├── pii.py # PII hash/encrypt/redact
│ │ └── audit.py # Audit logging
│ └── db/ # Database layer
│ ├── neo4j.py # Async Neo4j driver pool
│ └── redis.py # Redis connection
│ └── neo4j.py # Async Neo4j driver pool
├── domains/ # Domain specification packs
│ ├── plasticos/
│ ├── mortgage-brokerage/
Expand Down Expand Up @@ -153,7 +152,6 @@ l9-engine/
| Component | Version | Required Plugins |
|-----------|---------|------------------|
| Neo4j | 5.15+ Enterprise | APOC, Graph Data Science (GDS) |
| Redis | 7.x | — |
| Python | 3.12+ | — |

**Neo4j Plugins:**
Expand All @@ -174,7 +172,6 @@ Consistent across all L9 repos. Set in `.env` (local) or SSM Parameter Store (pr
| `NEO4J_USERNAME` | `neo4j` | Neo4j username |
| `NEO4J_PASSWORD` | — | Neo4j password (SSM in prod) |
| `NEO4J_DATABASE` | `neo4j` | Default database |
| `REDIS_URL` | `redis://localhost:6379/0` | Redis connection |
| `API_PORT` | `8000` | API listen port |
| `API_WORKERS` | `4` | Uvicorn workers |
| `DOMAINS_ROOT` | `./domains` | Path to domain specs |
Expand Down
2 changes: 0 additions & 2 deletions Readme-Requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@ status: active

- `neo4j_uri`, `neo4j_username`, `neo4j_password` → consumed by `dependencies.py` to init `GraphDriver`[^1]
- `domains_root` → consumed by `dependencies.py` to init `DomainPackLoader`[^2]
- `redis_url` → lazy-init Redis client
- Scoring weights (`w_structural`, `w_geo`, etc.) → match the spec's config reference[^3]
- Decay half-lives → match the temporal decay system in `engine-core-modules.py`[^1]

**`dependencies.py`** provides the `Depends()` functions that `match.py`, `sync.py`, and `health.py` already import:[^4]

- `get_graph_driver()` → returns the shared async Neo4j driver
- `get_domain_loader()` → returns the cached domain pack loader
- `get_redis()` → lazy Redis with graceful degradation
- `startup()` / `shutdown()` → called from `create_app()` lifespan

**`requirements.txt`** pins every dep from `pyproject.toml` with `>=X,<Y` bounds — works for Docker builds and non-Poetry envs .
Expand Down
8 changes: 4 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
## Issue unblock (session reference)

**Cluster:** CEG#138 CLOSED on PR 248; CEG#139 CLOSED already-fixed
**Owning fix:** https://github.com/Quantum-L9/Cognitive.Engine.Graphs/pull/248
**Next:** pause issues for this repo; PR remediator later
**Pickup:** Graphiti PICKUP written 2026-08-30
**Cluster:** Quantum-L9/Cognitive.Engine.Graphs#273 (+ #274 leftover, #275, #276, #277 duplicate of #276, #279)
**Owning fix:** leftover consumer `.l9/ci.json` (`repo_class: python`) stacked on PR 280
**Next:** do not merge from this skill; leftover issues already CLOSED; remaining CEG HUMAN issues stay OPEN
**Pickup:** Graphiti PICKUP written 2026-09-19

<!-- L9_META
l9_schema: 1
Expand Down
Loading
Loading