Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
Touch only what you must. Clean up only your own mess.
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
Following rules are here to help the AI avoid the same mistakes again:
- Run the dev stack with
docker compose up --build --watchso source changes rebuild/sync into the container. - Sortable table columns: date and numeric columns default to descending order on first click; text columns default to ascending.
- All times are UTC everywhere in the app and the DB; only convert to the user's local timezone when displaying to them. DB timestamp columns are stored as naive UTC (no tzinfo) so values behave identically on SQLite and PostgreSQL. Always get "now" from
lumen.timeutils.utcnow()(returns naive UTC) — never usedatetime.now(timezone.utc).replace(tzinfo=None)ordatetime.utcnow()directly, and usedefault=utcnowondb.DateTimecolumns. (Exception:request_logs.timeistimestamptz/aware because it is a TimescaleDB hypertable partition key.) In templates, emit<span class="local-datetime" data-utc="{{ dt.strftime('%Y-%m-%dT%H:%M:%SZ') }}"></span>and let the JS in app.js convert it to local time. Never hardcode "UTC" in displayed timestamps. - Dependencies are managed with uv, code is run with uv
- Imports at top of file only; except inside Flask app factory (
create_app) where deferred imports are required to avoid circular imports. - Styles and colors match University of Illinois, see https://builder3.toolkit.illinois.edu/getting_started/index.html
- When adding variables to config.yaml, make sure they are hot loaded if possible or print a warning
- Track changes in the CHANGELOG.md, if no unreleased section exists, then add it, make sure to not add a new subsection.
- Keep CHANGELOG entries short: one terse bullet per logical change, present tense, user-facing language. Do not write long narrative paragraphs explaining the "why" at length. Where a change has an obvious PR, link it (e.g.
(#123)), or an issue in GitHub. - When bumping the version in pyproject.toml, also run
uv lockto update uv.lock. - When bumping the version for a release, also update
versionandappVersionin chart/Chart.yaml to match. - The monitor in
scripts/monitoris versioned in lockstep with Lumen: when bumping the version for a release, set the same version inscripts/monitor/pyproject.tomland runuv lockinscripts/monitortoo. - When cutting a release, first pull the latest dependencies (
uv lock --upgrade) and then run the full test suite before tagging — upgraded dependencies can introduce breaking changes, so the release must not go out unless the tests pass. - "The full test suite" always includes the PostgreSQL and Redis tests; a plain
uv run pytestsilently skips them. Start throwaway services and point the suite at them:Check thedocker run -d --rm --name lumen-test-pg -e POSTGRES_USER=lumen -e POSTGRES_PASSWORD=lumen -e POSTGRES_DB=lumen_ci -p 55433:5432 timescale/timescaledb:2.27.2-pg17 docker run -d --rm --name lumen-test-redis -p 56380:6379 redis:7 LUMEN_TEST_POSTGRES_URL=postgresql://lumen:lumen@localhost:55433/lumen_ci LUMEN_TEST_REDIS_URL=redis://localhost:56380/0 uv run pytest -rs docker stop lumen-test-pg lumen-test-redis
-rsskip summary: no test may be skipped for a missingLUMEN_TEST_POSTGRES_URLorLUMEN_TEST_REDIS_URL. - When uploading the changelog for a release, add Contributors at the bottom as a list with a link to their github profile, icon and name.
- For local testing without OAuth or a real LLM: set
app.dev_userin config.yaml and useuv run dummy(dummy backend on port 9999). See the "Local Development" section in README.md. - Keep the help docs in
docs/up to date: any change to a user-facing screen (chat, profile, usage, models, model detail, projects, admin) must be reflected in the matching page underdocs/guides/,docs/models/,docs/projects/, ordocs/admin/, including re-capturing affected screenshots (see next rule). Schema changes go indocs/dbschema.md(see below). - New documentation added to docs, should also be reflected in mkdocs.yml
- Screenshots for the help docs live in
docs/img/. Re-capture them with playwright after any UI change that affects chat, usage, models, model detail, or projects pages. UseCONFIG_YAML=./dev.config.yaml uv run python -c "from lumen import create_app; ..."with a dev config that hasdev_userset. - Every new page (a route that renders HTML) must be added to
ROUTESinscripts/responsive_check.py(with the users who can see it, and toEXPECTED_STATUS/EXPECTED_REDIRECTSif it does not answer 200 at its own path) and toPAGESintests/ui/test_responsive_markup.py. Run the audit for it (ONLY=<name>) and make sure it passes at all four sizes before the PR. - SQLAlchemy: never use
Model.queryordb.session.query()— both are deprecated in SQLAlchemy 2.x and emitLegacyAPIWarning. Usedb.session.execute(select(Model)...)for all queries,.scalars().all()/.scalar_one_or_none()for results,db.first_or_404(stmt)for 404 guards,db.session.get(Model, pk)for PK lookups,db.get_or_404(Model, pk)when a missing row should 404. Model definitions also use 2.x style: declare columns asname: Mapped[T] = mapped_column(...)(useMapped[Optional[T]]for nullable columns) and relationships asMapped[list["X"]]/Mapped[Optional["X"]]/DynamicMapped["X"]— never the legacydb.Column(...)/db.relationship(...)form. Keep the explicit type inmapped_column(e.g.db.BigInteger,db.Numeric(12, 6),db.Text) so it is not lost to annotation inference. - DB connection hygiene — narrow rule, don't over-apply: ordinary (non-generator) request handlers should keep relying on Flask-SQLAlchemy's implicit per-request teardown to release
db.session— that mechanism is correct and well-tested; don't add defensivedb.session.remove()/commit/rollback calls to plain views that don't need them. The one rule that is absolute: no DB session and no Flask context (app or request) may span ayield, in any generator. This has bitten production twice:- A generator that interleaves DB calls with multiple
yields and is driven by something other than Flask's request/response cycle (e.g. a PrometheusCollector.collect()consumed bygenerate_latest()) must release its session explicitly in afinally—/metrics'LumenDBCollector.collect()leaked a connection idle-in-transaction until Postgres killed it, surfacing asserver closed the connection unexpectedlyon an unrelated request. - A streaming response-body generator must run context-free.
stream_with_contextis banned (enforced bytests/unit/test_no_stream_with_context.py): it re-pushes the request's app context onto whichever WSGI worker thread iterates the body, and a client disconnect abandons the generator so the push is never undone — the thread is permanently poisoned, every later request on it reuses the stuck context, and its session teardown never runs (one leaked idle-in-transaction connection per poisoned thread; Flask swallows the error so nothing is logged). Instead: captureapp = current_app._get_current_object()and every needed scalar in the view while the request context is still current, then inside the generator wrap each DB phase in its own short-livedwith app.app_context():that closes before the nextyield. Seellm.py'ssend_message_streamand the streaming paths inchat/routes.py/api/routes.pyfor the template.
- A generator that interleaves DB calls with multiple
- updates to the schema should be reflected in docs/dbschema.md and columns/tables should have comments.
- Don't use bare integer HTTP codes, use
HTTPStatusconstants. - updates to config file should be reflected in the helm chart values.yaml and values.schema.json.
All HTML must be WCAG 2.1 AA compliant. This is not optional.
You must have tests that check the code for compliance, and these tests must pass.
When writing or modifying HTML/JS:
- ARIA labels: Every interactive element needs an accessible name. Icon-only buttons (
✕,☰,←,ⓘ) requirearia-label. Never rely ontitlealone. - Form labels: Every
<input>,<select>, and<textarea>must have an associated<label>with afor/idpair. Useclass="visually-hidden"if the label should not be visible. Placeholder text is not a label substitute. - Modals: Add
aria-labelledbypointing to the modal title'sid. Close buttons needaria-label="Close". - Dynamic content: Use
aria-live="polite"(orrole="log") for areas updated via JS (chat messages, status results). Userole="alert" aria-live="assertive"for error/success feedback. - Keyboard access: If an element has a click handler, it must also be focusable (
tabindex="0") with akeydownhandler for Enter/Space, or use a native<button>/<a>. Hover-only visibility must also trigger on:focus. - Color contrast: Normal text needs 4.5:1 minimum. Do not use UIUC orange (
#e84a27) as a background with white text (only 3.0:1). Use#b5300cor darker. Verify with a contrast checker. - Headings: Maintain proper hierarchy (h1 → h2 → h3). Never skip from h1 to h5.
- Tables: Data tables need
<caption>(useclass="visually-hidden"if needed). Empty<th>cells for action columns need<span class="visually-hidden">Actions</span>. - Emoji as content: Wrap meaningful emoji in
<span role="img" aria-label="description">. - Skip navigation: SkipTo.js is loaded in
base.htmlandlanding.htmlfrom its CDN (skipto-landmarks-headings.github.io/page-script-5). Do not remove it. - Timed content: Auto-dismiss timers must be at least 20 seconds and pause on hover/focus (WCAG 2.2.1).
- Overflow: Never use
overflow:hiddenon scrollable content containers — useoverflow:autoto prevent clipping at zoom. - Focus management: After JS changes page content (loading data, deleting items), move focus to a logical target (e.g., input field, action button).
- Active/selected state: Never convey state through color alone. Combine with a border, icon, bold, or
aria-selected/aria-current.
This project is indexed by GitNexus as lumen (4912 symbols, 11675 relationships, 273 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
Index stale? Run
node .gitnexus/run.cjs analyzefrom the project root — it auto-selects an available runner. No.gitnexus/run.cjsyet?npx gitnexus analyze(npm 11 crash →npm i -g gitnexus; #1939).
- MUST run impact analysis before editing any symbol. Before modifying a function, class, or method, run
impact({target: "symbolName", direction: "upstream"})and report the blast radius (direct callers, affected processes, risk level) to the user. - MUST run
detect_changes()before committing to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch:detect_changes({scope: "compare", base_ref: "main"}). - MUST warn the user if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use
query({search_query: "concept"})to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use
context({name: "symbolName"}). - For security review,
explain({target: "fileOrSymbol"})lists taint findings (source→sink flows; needsanalyze --pdg).
- NEVER edit a function, class, or method without first running
impacton it. - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use
renamewhich understands the call graph. - NEVER commit changes without running
detect_changes()to check affected scope.
| Resource | Use for |
|---|---|
gitnexus://repo/lumen/context |
Codebase overview, check index freshness |
gitnexus://repo/lumen/clusters |
All functional areas |
gitnexus://repo/lumen/processes |
All execution flows |
gitnexus://repo/lumen/process/{name} |
Step-by-step execution trace |
| Task | Read this skill file |
|---|---|
| Understand architecture / "How does X work?" | .claude/skills/gitnexus/gitnexus-exploring/SKILL.md |
| Blast radius / "What breaks if I change X?" | .claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md |
| Trace bugs / "Why is X failing?" | .claude/skills/gitnexus/gitnexus-debugging/SKILL.md |
| Rename / extract / split / refactor | .claude/skills/gitnexus/gitnexus-refactoring/SKILL.md |
| Tools, resources, schema reference | .claude/skills/gitnexus/gitnexus-guide/SKILL.md |
| Index, status, clean, wiki CLI commands | .claude/skills/gitnexus/gitnexus-cli/SKILL.md |