Skip to content

Latest commit

 

History

History
571 lines (466 loc) · 38.2 KB

File metadata and controls

571 lines (466 loc) · 38.2 KB

CLAUDE.md

Guidance for Claude Code working with this repository.

Quick Start

uv sync --all-extras                # Install dependencies
uv run pre-commit install           # Setup hooks (installs both pre-commit AND pre-push)
uv run playwright install chromium  # Headless browser for Prefab UI render tests
npm --prefix packages/katana-client ci  # TS client deps (needed for regenerate-all / generate-ts)
cp .env.example .env                # Add KATANA_API_KEY

Worktrees: re-run uv run pre-commit install after git worktree add — pre-commit hooks aren't shared across worktrees. The pre-push-guard.sh that blocks unintended pushes to main (see Known Pitfalls) only fires when the hook is installed in the worktree's .git/hooks/.

For ongoing work pickup, open the Rolling Backlog board first — it's the canonical answer to "what should I work on?" See Project Backlog below for the Priority / Workstream conventions and the issue-status contract.

Essential Commands

Command Time When to Use
uv run poe quick-check ~5-10s During development
uv run poe agent-check ~8-12s Before committing
uv run poe check ~75s Before opening PR (incl browser)
uv run poe full-check ~85s Before requesting review
uv run poe fix ~5s Auto-fix lint issues
uv run poe test ~16s Run tests (4 workers, no browser)
uv run poe test-browser ~60s Headless Prefab UI render tests

NEVER CANCEL long-running commands - they may appear to hang but are processing.

CRITICAL: Zero Tolerance for Ignoring Errors

FIX ALL ISSUES. NO EXCEPTIONS.

  • NO noqa, type: ignore, exclusions, or skips
  • NO "pre-existing issues" or "unrelated to my changes" excuses
  • NO --no-verify commits
  • ASK for help if blocked - don't work around errors

Proper fixes:

  • Too many parameters? → Create a dataclass
  • Name shadows built-in? → Rename it
  • Circular import? → Use TYPE_CHECKING block

Verify Your Work

Always run the appropriate validation tier before considering work complete. See the Essential Commands table above - use quick-check during development, agent-check before committing, and check before opening a PR. Don't trust that code works just because it looks right.

Project Backlog

Board: Katana MCP — Rolling Backlog (project #5) is the durable answer to "what's next?" — not gh issue list. The board classifies every open issue across four dimensions (Priority / Effort / Workstream / Umbrella) and surfaces them in three views (List, Board, Roadmap). Every new Issue and PR auto-adds in the Todo column with unset Priority/Workstream until a human triages.

Session-start anchor

First thing in a session: glance at the Board view. Look at the In Progress column (anything already running) and the top of Todo (P0 and P1). If the user asks "what should I work on?", the answer is the top of Todo. Do not re-derive the queue from gh issue list — the board is the source of truth.

Priority bucket definitions

Priority Definition Examples
P0-now Breaks a workflow that's already shipped; user-visible failure; should not survive the day. #527 (every PATCH raises), #547 (browser bail-out for serial-tracked)
P1-this-week Real value, no acute breakage, but compounding cost if deferred. #500 (timeouts), #503 (silent drop), umbrella-driven workstream advances
P2-soon Worth doing; not blocking; has a path. Card sub-issues, audits, secondary umbrellas
P3-someday Backlog parking; revisit on next groom; close if stale. Dead skill ideas, infrastructure ideas without a forcing function

Workstream definitions

  • Cards — Prefab UI builders + per-entity card design (driven by #537).
  • Cache — typed cache, FTS5, sync, cold-cache recovery (driven by #472, #473).
  • Fulfillmentfulfill_order / receive_purchase_order / order lifecycle. Orthogonal to umbrellas.
  • Spec-drift — Katana API spec misalignment, codegen issues.
  • Custom-fields — Custom-fields vertical: spec alignment, MCP tooling, definition CRUD, search-endpoint coverage. Spans both client and server.
  • Harness.claude/, harness-kit integration, skills, agents.
  • Process — backlog hygiene, docs, ADRs, retros.
  • Other — anything that doesn't fit above; usually means the schema needs a new bucket.

Status contract

  • Issue → In Progress when a linked PR opens. Use a GitHub closing keyword in the PR body to create the link: Closes #N is the canonical form for this repo (Fixes #N / Resolves #N also work — GitHub recognizes all three as closing keywords). Refs #N and See #N are references, not links — they don't trigger the auto-move workflow and won't auto-close the issue on merge.
  • Issue → Done when the linked PR merges, regardless of whether the issue itself is closed — issues may stay open as parents to future sub-work.
  • Status moves are driven by GitHub's built-in project workflows; the auto-add-workflow keeps new Issues + PRs on the board so nothing slips through.

Adding to the board manually

If the auto-add workflow misses something (rare), or to retroactively classify a pre-board issue, use the gh CLI:

gh project item-add 5 --owner @me --url <issue-or-pr-url>
# then set Priority + Workstream via gh project item-edit --single-select-option-id

The field IDs / option IDs are stable; gh project field-list 5 --owner @me --format json prints them.

Grooming the board

Use /groom to surface drift between issues and project state — closed issues still in Todo, items in Done with open issues, P3 items stale for >90 days, P0/P1 sitting idle >21 days, items missing Priority or Workstream. The skill reads the board, classifies items across five heuristic categories, and asks for per-category confirmation before applying any mutation. Destructive mutations (closures) require a second per-issue confirmation — category-level approval is enough for status moves and priority shifts, but /groom won't bulk-close on a single OK. Don't re-derive the priority queue from gh issue list/groom evolves the board forward.

Continuous Improvement

This file is meant to evolve. Update it when you learn something that would help future sessions:

  • Fix a tricky bug? Add the root cause to Known Pitfalls.
  • Discover a new anti-pattern? Add it to Anti-Patterns to Avoid.
  • Find a command or workflow that's missing? Add it to Essential Commands or Detailed Documentation.
  • Hit a confusing API behavior? Document it so the next session doesn't waste time rediscovering it.

The same applies to all project documentation - if instructions are wrong, incomplete, or misleading, fix them as part of your current work rather than leaving them for later.

Known Pitfalls

This is a living document. When you discover a recurring mistake, add it where it fits topically — to one of the linked docs below if it's subsystem-scoped, or to the Cross-cutting list here if it's genuinely repo-wide.

Cross-cutting

  • A UI-emitting tool's model-critical data MUST live in structured_content, not only content — some hosts drop the content channel. make_tool_result puts the full response JSON in content and the Prefab card in structured_content. The MCP Apps prose says content is model-visible and structured_content is UI-only — but that is host-dependent and not guaranteed: Claude Code forwards only structuredContent to the model when both are present, dropping content[].text entirely (anthropics/claude-code#55677, closed "not planned"; claude.ai web currently sends both). Large payloads are a second drop path (Claude Code truncates MCP output above ~25k tokens via MAX_MCP_OUTPUT_TOKENS; claude.ai spills payloads above ~150k chars to a pointer). Consequence: any identifier the model needs for a follow-up action (row / line / address / fulfillment id, variant_id, discounts, tax IDs) must be in the PrefabApp's state=, never reduced to display-only cells there. The classic bug: a detail card doing state = {**entity, "rows": display_cells} — overwriting authoritative rows with formatted cells — which stripped SO row IDs + discounts and sent an agent scraping the Katana web UI. Fix pattern: put display cells under a separate key via with_display_rows(entity, cells) (refuses to clobber) and bind the DataTable to {{ <entity>.rows_display }}, leaving state.<entity>.rows authoritative. Read tools that return make_json_result (or a raw ToolResult with structured_content=response.model_dump()) are inherently safe — full data in both channels. Pinned by tests/test_prefab_ui.py::TestWithDisplayRows and ::TestBuildSoDetailUI::test_authoritative_row_data_survives_in_structured_content.

  • Multiple katana-mcp-server instances share one machine-wide SQLite cache — the "Unable to reach katana-erp-dev" reconnect loop. The typed cache defaults to a single path (platformdirs.user_cache_dir("katana-mcp")/typed_cache.db), so every running server — a Claude Desktop connector + a Claude Code .mcp.json session, or two Desktop connectors (katana-erp + katana-erp-dev) — opens the same file and all become writers. Recognizable signature: the MCP client reports "Unable to reach katana-erp-dev", the server log shows a tight tools/list reconnect loop with zero tools/call landing (reads succeed, writes hang), and the WAL grows. Diagnose with lsof ~/Library/Caches/katana-mcp/typed_cache.db — >1 server PID holding the same inode confirms it. Fixed in #974: the shared file now uses WAL + a 30s busy_timeout, and the sync write transactions are scoped tightly around the DB writes (never across the network fetch), so contention degrades to a brief stall instead of a client-visible hang. For hard isolation (a separate DB per connector, e.g. a dev tenant) set KATANA_CACHE_DIR — but prefer the shared warm default (an isolated cache starts cold, and Katana meters ~60 req/min per key). Pinned by katana_mcp_server/tests/test_typed_cache.py::TestConcurrentProcessSharing and ::TestCacheDirOverride.

  • Variants can have null SKUs — never assume Variant.sku (or ServiceVariant.sku) is non-null. Katana allows variants without SKUs (legacy NetSuite imports are a common source). The wire contract reflects this: Variant.sku and ServiceVariant.sku are str | None in both the attrs and pydantic models — the service vertical follows the exact same contract (a null-sku service variant nested in a get_all_services response previously crashed the typed-cache sync, emptying the catalog so every search_items call failed). Downstream consumers must coalesce — render with variant.sku or "", score with (variant.sku or "", weight). The KatanaVariant domain model and CachedVariant table both accept null SKU; get_by_sku won't match these rows (NULL ≠ string), so they're effectively unreachable by SKU lookup but still surface in ID-based reads and FTS fuzzy search. Both verticals are pinned by these tests — relaxing or re-tightening any SKU field must keep all four green:

    • tests/test_models_pydantic.py::TestVariantNullSku
    • tests/test_models_pydantic.py::TestServiceVariantNullSku
    • katana_mcp_server/tests/test_typed_cache_catalog.py::TestVariantPostprocess::test_sync_tolerates_null_sku_in_nested_variants
    • katana_mcp_server/tests/test_typed_cache_catalog.py::TestCatalogSync::test_sync_tolerates_null_sku_in_service_variants
  • Fake time in time-based tests — NEVER read the real wall clock. Any test whose assertion depends on elapsed time, a current timestamp, or an expiry/freshness window must fake time. Real-clock reads (time.time(), time.monotonic(), datetime.now(), date.today()) make tests flaky on slow/loaded CI and force tolerance bands (assert 3.5 <= elapsed <= 6.5) that paper over non-determinism instead of fixing it. The tells: a tolerance band, an abs(...) < 1 slop, or a before <= x <= after bracket around two clock reads. Two faking tools, picked by what time you're controlling:

    • Asyncio time (asyncio.sleep, loop.call_later, timeouts) → the looptime pytest plugin (@pytest.mark.looptime). It virtualizes the loop clock so asyncio.sleep(5) advances loop.time() by exactly 5 with zero real delay.
    • Wall-clock reads (datetime.now(), time.time()) → time_machine (with time_machine.travel(fixed_instant, tick=False): ...). It freezes now()/time.time() while keeping the real datetime class, so production isinstance(x, datetime) checks and constructors still work. Do NOT freeze time by monkeypatching the datetime name of the code under test (your own module) to a subclass — that swaps the class out from under that module's own isinstance/constructor calls and breaks them (cost us a debugging cycle here). Use time_machine for code you own. (Shimming a third-party library's datetime is a different, narrower move — see the looptime exception below.)
    • Both in one test → prefer NOT mixing time_machine + looptime. time_machine freezes time.time / datetime.now (it does not touch time.monotonic / perf_counter, which is what looptime builds its virtual clock on). The risk is the frozen time.time: asyncio reads it for some absolute timeout math, so a process-wide freeze can interact unpredictably with looptime's virtual selector. So for a test that needs both faked async sleep AND a faked wall-clock read, patch only the narrow wall-clock seam the code reads — monkeypatch the third-party module's datetime, or freeze time.time alone — rather than reaching for time_machine. Examples: tests/test_rate_limit_retry.py::...test_http_date_retry_after_paces_end_to_end (shims httpx_retries.retry.datetime) and tests/test_rate_limit_transport.py::...test_blocks_subsequent_request_until_gate_releases (freezes time.time).

    The one exception: harness code that genuinely waits on a real external process (browser-render polling, a subprocess dev-server) legitimately uses real time.monotonic() + time.sleep() — that's real I/O, not a logic-time assertion. Keep those (e.g. katana_mcp_server/tests/browser/conftest.py::_wait_http_ok), but never copy that pattern into a unit test.

  • Editing generated filesapi/**/*.py, models/**/*.py, client.py, models_pydantic/_generated/**, and models_pydantic/_auto_registry.py are generated. Other modules in models_pydantic/ (e.g. _base.py, _mapped_shim.py, _pydantic_json.py, _registry.py, converters.py) are hand-maintained. The TypeScript client's packages/katana-client/src/generated/** is generated too (via @hey-api/openapi-ts). All three clients regenerate from the same docs/katana-openapi.yaml, so a spec change must regenerate all of them — run uv run poe regenerate-all (which chains regenerate-client + generate-pydantic + generate-ts) instead of editing the generated paths directly. generate-ts needs the TS package's node_modules (npm ci in packages/katana-client, pinned by its committed package-lock.json).

    • Two different CI gates, because the generators differ in determinism. The Python generators are byte-identical across OSes, so the generated-files job regenerates them and fails on any git status drift under katana_public_api_client/. The TS generator shells out to native per-platform binaries (@esbuild/*, @biomejs/*), so its output is not byte-stable macOS↔Linux — a drift check would false-positive. The TS client is instead gated functionally by the typescript-client job: it typechecks + tests the committed client and re-runs generate + typecheck to prove a spec change can't leave the TS client broken. So: regenerate + commit the TS output (it won't be byte-checked), but a spec change that breaks the TS client will fail CI.
  • Raw list responses in tests — Katana wraps every list endpoint in {"data": [...]}. Never put raw arrays in mocks. Two documented exceptions return a bare JSON array on the wire (no data envelope): GET /bin_locations (verified live 2026-06-03, #575) and GET /user_info. Their generated parsers return list[StorageBinResponse] / the bare model directly, so mock those two with a top-level array ([ {...} ]), not {"data": [...]}.

  • Always call functions with keyword argumentsfunc(param=value), not func(value), for all calls. Especially prefab_ui components, which only accept kwargs.

  • Never delete .claude/worktrees/ — other agents may be working inside them. If a worktree causes tool noise (e.g., ty scanning into it), exclude the path in the tool's config — never rm -rf.

  • No hand-maintained drift-prone references in docs. Versions, endpoint counts, tool counts, ADR enumerations, "coming soon" callouts, and dated footers all drift the moment they're written. Use shields.io badges for versions, link to the ADR index README.md files instead of listing ADRs by name, link to katana://help/tools instead of enumerating tools, and link to the project board instead of citing issue numbers as roadmap markers. Full rule + reasoning lives in docs/CONTRIBUTING.md "No hand-maintained drift-prone references". This is the second time the README has gone significantly stale (#401 sweep → #569 sweep) — the structural fix is to remove the drift surface, not to be more diligent about updating it.

Topical — load the linked doc when working in that area

Each row keys the topic and lists the most-greppable terms inside, so a grep of this file for "DataTable" or "archive" or "ListResponse" lands you on the right pointer. Open the linked doc when you're working in that subsystem; agents loading CLAUDE.md don't need to pay for the full content of every topic.

Area Keywords inside Where to read
Prefab UI DataTable mustache {{ key }}, browser-test wire shape via make_tool_result, register_preview_tool + meta=UI_META contract, help resource drift katana_mcp_server/docs/prefab/README.md
Typed cache archive/deleted opt-in flags (include_archived / include_deleted), is_archived / is_deleted derived bools, cross-entity ID collisions (per-type maps), Cached<Name> siblings (don't pollute API spec), cross-table cache-only columns (post_sync backfill + preserve_columns_on_conflict, e.g. service_id) katana_mcp_server/docs/typed_cache/README.md
OpenAPI spec authoring OpenAPI 3.1 ($ref siblings legal), use-site descriptions, ListResponse schema for arrays, generator/spec regen lockstep + breaking-change markers, privacy (no real names/emails), fix-at-source rule, POST creates return 200 not 201 (else unwrap_as raises UnexpectedResponse on a successful mutation) katana_public_api_client/docs/spec-authoring.md
Spec-drift override registry audit-spec-strict CI gate, docs/upstream-specs/audit-overrides.yaml (4 categories: intentional / local_correct_upstream_wrong punch-list / both_wrong_live_correct / upstream_correct_local_wrong needs fix_tracked_in), narrow (endpoint, kind, field, values) matching, stale-override detection, --no-overrides for raw drift — every finding terminates in a fix or a registry entry docs/upstream-specs/README.md
API response handling unwrap_as, unwrap_data, is_success, unwrap_unset, to_unset, transport-layer retries, exception hierarchy (AuthenticationError, ValidationError, RateLimitError, ServerError, APIError) katana_public_api_client/docs/guide.md — "Response Handling" section
Commit / push safety first-push HEAD:refs/heads/<name> form, uv.lock drift bundling, generator regen lockstep, breaking-change marker (feat(client)!: + BREAKING CHANGE: footer) .github/agents/guides/shared/COMMIT_STANDARDS.md
Rebase before PR / before addressing review git fetch origin <base> && git log HEAD..origin/<base>, mergeStateStatus=BEHIND auto-merge stall Enforced by /open-pr and /review-pr CRITICAL sections — no separate doc needed

Using the LSP tool

Both Python (pyright) and TypeScript (typescript-language-server) LSPs are configured and active. Prefer LSP operations over Read + Grep for type and call-graph questions — they are faster, more accurate, and cross-reference the real type system (including third-party libraries in .venv).

When you need to… Use
Understand a symbol's type/signature/docstring LSP hover
Jump to where a function/class is defined (project code) LSP goToDefinition
Find every caller of a function before changing its signature LSP findReferences
List all symbols in a file (skim without reading all of it) LSP documentSymbol
Trace callers of a function (who calls X?) LSP incomingCalls
Trace callees of a function (what does X call?) LSP outgoingCalls

Project root must match workspace root. The pyright config lives at pyrightconfig.json (relative venvPath: "."). CLI pyright uses it automatically (npx pyright or uv run pyright); the langserver reads it on startup.

LSP known limitations

  • workspaceSymbol returns nothing in this tooling — pyright only indexes open files, and the LSP tool doesn't expose the query parameter. Use Grep for project-wide symbol search instead (e.g., Grep "def format_md_table").
  • goToImplementation is not implemented by pyright — use goToDefinition instead.
  • goToDefinition on external-library imports returns "no definition found" — use hover instead, which gives you the class signature + docstring.
  • If hover returns Unknown for a project-external import (e.g., pydantic, fastmcp), the langserver is stale — flag to the user that Claude Code needs a restart to re-read pyrightconfig.json. All project-internal imports should always resolve.

Handling <new-diagnostics> system reminders

After each edit, the harness may surface a <new-diagnostics> block listing pyright/ruff diagnostics on the file. Treat these as real until proven otherwise — both pyright and ty run in CI (uv run poe lint), so an LSP-flagged error will likely break the build.

The protocol when a <new-diagnostics> block appears:

  1. Check pyright CLI on the affected fileuv run pyright <path>. Pyright CLI is the source of truth; the LSP can be stale (especially after generator regen, stash/pop, or rebase).

  2. If pyright CLI agrees, fix it. Don't push code with pending pyright errors — CI will fail.

  3. If pyright CLI disagrees, the LSP is stale. Note it explicitly in your reply to the user ("LSP shows X but pyright CLI is clean — likely stale after regen") so they know whether their editor needs a restart. Do NOT silently move on without verifying.

  4. Stale-LSP common triggers — running uv run poe generate-pydantic, rebasing onto main, or stashing & popping. Pyright re-indexes lazily; the system reminder may show pre-edit state until the next save.

  5. Cross-worktree LSP bleed — when a sub-agent is operating in a sibling git worktree (e.g., the repo root /Users/dougborg/Projects/katana-openapi-client/ while your session lives in .claude/worktrees/<name>/), the pyright LSP daemon stays rooted at the session's original primary cwd and never switches when other agents enter sibling worktrees. The daemon's snapshot reflects one branch's files; your edits target another branch's files. <new-diagnostics> reminders then surface phantom errors that complain about fields / params that exist on one branch but not the other. Symptom: uv run pyright <path> from your worktree is clean while LSP keeps flagging mismatches across a wide swath of files you didn't touch.

    Workaround: trust uv run pyright <path> from your worktree as ground truth; ignore the LSP diagnostics until the sub-agent's PR lands and you rebase. Don't try to "fix" the phantom diagnostics — the code is correct on your branch.

    Why this isn't fixable here: the pyright-lsp plugin can't accept a per-call projectRoot (Anthropic claude-code#32230, closed as not planned). The LSP plugin layer doesn't even receive a workspace rootUri during init for per-project config discovery (#27220). Subagents resolve to the main repo root rather than their worktree (#31546); session worktrees go inside the repo root by default (#49986). All four upstream issues are closed without fix as of this writing.

The repo's house rule (no # type: ignore / # noqa) means a real diagnostic is always a fix, never a suppression. If the diagnostic is wrong (third-party stub gap), document the rationale and use a config-level rule override in pyrightconfig.json (already done for reportIncompatibleVariableOverride and reportAssignmentType — see sync_state.py).

Architecture Overview

Monorepo with 3 packages:

  • katana_public_api_client/ - Python client with transport-layer resilience
  • katana_mcp_server/ - MCP server for AI assistants
  • packages/katana-client/ - TypeScript client

Key pattern: Resilience (retries, rate limiting, pagination) is implemented at the httpx transport layer - ALL 100+ API endpoints get it automatically.

File Rules

Category Files Action
EDITABLE katana_client.py, tests/, scripts/, docs/ Can modify
GENERATED api/**/*.py, models/**/*.py, client.py DO NOT EDIT

Regenerate client: uv run poe regenerate-client (2+ min)

Commit Standards

feat(client): add feature    # Client MINOR release
fix(mcp): fix bug            # MCP PATCH release
docs: update README          # No release

Use ! for breaking changes: feat(client)!: breaking change

Claude Code Harness

The harness lives in .claude/skills/ (workflows) and .claude/agents/ (delegated sub-agents). Provenance is tracked in .harness-lock.json. The project's upstream is configured in .claude/harness-upstream (dougborg/harness-kit) — used by /harness-issue (when the plugin is installed) to file Issues / PRs against the right repo.

Plugin baseline: the project tracks harness-kit at the version recorded in .harness-lock.json (sources.harness-kit.version). The .claude/ files were originally seeded via /harness bootstrap and the two upstream-sourced agents (code-reviewer, verifier) are intentionally locally modified with Katana-specific content — /harness update knows to leave them alone.

To enable the plugin (one-time per developer):

/plugin marketplace add dougborg/harness-kit
/plugin install harness-kit@harness-kit

.claude/settings.json already lists harness-kit@harness-kit under enabledPlugins, so once installed locally the plugin auto-enables for this repo. The install itself isn't auto-bootstrapped from a clone (Claude Code doesn't install plugins on clone) — but the enable step is shared, so contributors only need to run the two /plugin commands above once.

After install, the namespaced skills (/harness-kit:harness, /harness-kit:harness-issue, /harness-kit:standup, /harness-kit:feature-spec, /harness-kit:skill-writer, etc.) become available. To pull a newer baseline into .claude/ after upgrading the plugin, run /harness update.

Project-local skills below shadow / extend the plugin's generic versions where they exist; both are valid (use the project-local /open-pr for the Katana flavor; use harness-kit:open-pr for a generic fallback).

Skills (slash commands)

Skill Purpose
/pre-commit Fast pre-flight before git commit
/review Branch review delegating to code-reviewer agent
/techdebt Scan for tech debt and anti-patterns (reports only)
/write-tests Write tests with AAA pattern + edge-case checklist
/generate-docs Generate or update docstrings, ADRs, READMEs
/verify Skeptical end-to-end verification
/open-pr Open PR: validate, self-review, push, wait for CI
/review-pr Address PR review comments: fix, push, reply in thread

Agents (delegated work)

Spawn via the Agent tool with subagent_type set to the agent name.

Agent Model Purpose
code-reviewer sonnet Six-dimension read-only review of the current diff
verifier haiku Mechanical pre-PR gate (validation, debug code, history)
domain-advisor sonnet Read-only Q&A on Katana conventions (UNSET, unwrap, helpers)
code-modernizer sonnet Actively rewrites Katana-specific anti-patterns
pr-preparer haiku Project-specific PR readiness (coverage, ADRs, help-resource)
spec-auditor sonnet Audit local OpenAPI spec vs upstream Katana API

Recommended PR pipeline: code-modernizerverifierpr-preparercode-reviewer/open-pr.

Automation hooks

Configured in .claude/settings.local.json (gitignored):

  • PostToolUse / Edit|Write|MultiEdit — runs uv run poe fix silently after any Python file is edited. Fixes lint/format issues before Claude reads the result.
  • Stop — when the session touched more than 3 files, prints a reminder to run /harness retro to capture learnings. (When the harness-kit plugin is installed, also consider /session-retro — see harness-kit#30 — for project-side learnings.)

Test environment

Anything that talks to a live Katana tenant from test code, probes, or CI smoke tests goes through make_test_client() in katana_public_api_client/testing.py — not KatanaClient() directly. The helper reads KATANA_TEST_API_KEY (required) and KATANA_TEST_BASE_URL (optional, defaults to the prod base URL — same Katana deployment, different tenant).

Safety rule: no silent fallback to KATANA_API_KEY. If KATANA_TEST_API_KEY is unset, the helper raises RuntimeError. Falling back to the prod key would let a misconfigured CI run mutate production state — exactly the failure mode the helper exists to prevent. Tests that want graceful skipping wire the skip in the fixture, not in the helper.

Set the env vars in .env (uncommented in .env.example). Phase 1 landed the helper, .env updates, and probe-script refactors. Phase 2 added the live client suite at tests/integration/ — read-only smoke tests built on make_test_client(), run via uv run poe test-integration-live, auto-skipping when KATANA_TEST_API_KEY is unset (the skip lives in the live_client fixture, not the helper). See tests/integration/README.md for the SDT-tagging + cleanup contract any write test must follow. Phase 3 added the live-integration.yml workflow (nightly + workflow_dispatch + needs-live-test-labeled PRs; soft-fail; needs the KATANA_TEST_API_KEY repo secret). Phase 4 added live MCP-server smoke tests at katana_mcp_server/tests/smoke/ — read-only MCP tool impls exercised through a real Services (client + typed cache), run via uv run poe test-smoke-mcp (marker smoke) and a parallel mcp-smoke job in the workflow. Track further progress on the project board.

Detailed Documentation

Discover on-demand - read these when working on specific areas:

Topic File
Validation tiers .github/agents/guides/shared/VALIDATION_TIERS.md
Commit standards .github/agents/guides/shared/COMMIT_STANDARDS.md
File organization .github/agents/guides/shared/FILE_ORGANIZATION.md
Architecture .github/agents/guides/shared/ARCHITECTURE_QUICK_REF.md
Client guide katana_public_api_client/docs/guide.md
OpenAPI spec authoring katana_public_api_client/docs/spec-authoring.md
Test environment katana_public_api_client/testing.py (make_test_client())
MCP docs katana_mcp_server/docs/README.md
Modify-pipeline timing katana_mcp_server/docs/LOGGING.md — "Modify-pipeline sub-step timing"
Prefab UI pitfalls katana_mcp_server/docs/prefab/README.md
Typed cache patterns katana_mcp_server/docs/typed_cache/README.md
TypeScript client packages/katana-client/README.md
ADRs docs/adr/README.md