Guidance for Claude Code working with this repository.
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_KEYWorktrees: 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.
| 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.
FIX ALL ISSUES. NO EXCEPTIONS.
- NO
noqa,type: ignore, exclusions, or skips - NO "pre-existing issues" or "unrelated to my changes" excuses
- NO
--no-verifycommits - 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_CHECKINGblock
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.
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.
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 | 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 |
- Cards — Prefab UI builders + per-entity card design (driven by #537).
- Cache — typed cache, FTS5, sync, cold-cache recovery (driven by #472, #473).
- Fulfillment —
fulfill_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.
- Issue → In Progress when a linked PR opens. Use a GitHub closing keyword in the PR
body to create the link:
Closes #Nis the canonical form for this repo (Fixes #N/Resolves #Nalso work — GitHub recognizes all three as closing keywords).Refs #NandSee #Nare 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.
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-idThe field IDs / option IDs are stable;
gh project field-list 5 --owner @me --format json prints them.
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.
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.
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.
-
A UI-emitting tool's model-critical data MUST live in
structured_content, not onlycontent— some hosts drop thecontentchannel.make_tool_resultputs the full response JSON incontentand the Prefab card instructured_content. The MCP Apps prose sayscontentis model-visible andstructured_contentis UI-only — but that is host-dependent and not guaranteed: Claude Code forwards onlystructuredContentto the model when both are present, droppingcontent[].textentirely (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 viaMAX_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 / fulfillmentid,variant_id, discounts, tax IDs) must be in the PrefabApp'sstate=, never reduced to display-only cells there. The classic bug: a detail card doingstate = {**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 viawith_display_rows(entity, cells)(refuses to clobber) and bind the DataTable to{{ <entity>.rows_display }}, leavingstate.<entity>.rowsauthoritative. Read tools that returnmake_json_result(or a rawToolResultwithstructured_content=response.model_dump()) are inherently safe — full data in both channels. Pinned bytests/test_prefab_ui.py::TestWithDisplayRowsand::TestBuildSoDetailUI::test_authoritative_row_data_survives_in_structured_content. -
Multiple
katana-mcp-serverinstances 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.jsonsession, 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 tighttools/listreconnect loop with zerotools/calllanding (reads succeed, writes hang), and the WAL grows. Diagnose withlsof ~/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 30sbusy_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) setKATANA_CACHE_DIR— but prefer the shared warm default (an isolated cache starts cold, and Katana meters ~60 req/min per key). Pinned bykatana_mcp_server/tests/test_typed_cache.py::TestConcurrentProcessSharingand::TestCacheDirOverride. -
Variants can have null SKUs — never assume
Variant.sku(orServiceVariant.sku) is non-null. Katana allows variants without SKUs (legacy NetSuite imports are a common source). The wire contract reflects this:Variant.skuandServiceVariant.skuarestr | Nonein both the attrs and pydantic models — the service vertical follows the exact same contract (a null-sku service variant nested in aget_all_servicesresponse previously crashed the typed-cache sync, emptying the catalog so everysearch_itemscall failed). Downstream consumers must coalesce — render withvariant.sku or "", score with(variant.sku or "", weight). TheKatanaVariantdomain model andCachedVarianttable both accept null SKU;get_by_skuwon'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::TestVariantNullSkutests/test_models_pydantic.py::TestServiceVariantNullSkukatana_mcp_server/tests/test_typed_cache_catalog.py::TestVariantPostprocess::test_sync_tolerates_null_sku_in_nested_variantskatana_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, anabs(...) < 1slop, or abefore <= x <= afterbracket around two clock reads. Two faking tools, picked by what time you're controlling:- Asyncio time (
asyncio.sleep,loop.call_later, timeouts) → thelooptimepytest plugin (@pytest.mark.looptime). It virtualizes the loop clock soasyncio.sleep(5)advancesloop.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 freezesnow()/time.time()while keeping the realdatetimeclass, so productionisinstance(x, datetime)checks and constructors still work. Do NOT freeze time by monkeypatching thedatetimename of the code under test (your own module) to a subclass — that swaps the class out from under that module's ownisinstance/constructor calls and breaks them (cost us a debugging cycle here). Usetime_machinefor code you own. (Shimming a third-party library'sdatetimeis a different, narrower move — see the looptime exception below.) - Both in one test → prefer NOT mixing
time_machine+looptime.time_machinefreezestime.time/datetime.now(it does not touchtime.monotonic/perf_counter, which is whatlooptimebuilds its virtual clock on). The risk is the frozentime.time: asyncio reads it for some absolute timeout math, so a process-wide freeze can interact unpredictably withlooptime'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'sdatetime, or freezetime.timealone — rather than reaching fortime_machine. Examples:tests/test_rate_limit_retry.py::...test_http_date_retry_after_paces_end_to_end(shimshttpx_retries.retry.datetime) andtests/test_rate_limit_transport.py::...test_blocks_subsequent_request_until_gate_releases(freezestime.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. - Asyncio time (
-
Editing generated files —
api/**/*.py,models/**/*.py,client.py,models_pydantic/_generated/**, andmodels_pydantic/_auto_registry.pyare generated. Other modules inmodels_pydantic/(e.g._base.py,_mapped_shim.py,_pydantic_json.py,_registry.py,converters.py) are hand-maintained. The TypeScript client'spackages/katana-client/src/generated/**is generated too (via@hey-api/openapi-ts). All three clients regenerate from the samedocs/katana-openapi.yaml, so a spec change must regenerate all of them — runuv run poe regenerate-all(which chainsregenerate-client+generate-pydantic+generate-ts) instead of editing the generated paths directly.generate-tsneeds the TS package'snode_modules(npm ciinpackages/katana-client, pinned by its committedpackage-lock.json).- Two different CI gates, because the generators differ in determinism. The Python
generators are byte-identical across OSes, so the
generated-filesjob regenerates them and fails on anygit statusdrift underkatana_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 thetypescript-clientjob: it typechecks + tests the committed client and re-runsgenerate+typecheckto 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.
- Two different CI gates, because the generators differ in determinism. The Python
generators are byte-identical across OSes, so the
-
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 (nodataenvelope):GET /bin_locations(verified live 2026-06-03, #575) andGET /user_info. Their generated parsers returnlist[StorageBinResponse]/ the bare model directly, so mock those two with a top-level array ([ {...} ]), not{"data": [...]}. -
Always call functions with keyword arguments —
func(param=value), notfunc(value), for all calls. Especiallyprefab_uicomponents, which only accept kwargs. -
Never delete
.claude/worktrees/— other agents may be working inside them. If a worktree causes tool noise (e.g.,tyscanning into it), exclude the path in the tool's config — neverrm -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.mdfiles instead of listing ADRs by name, link tokatana://help/toolsinstead 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.
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 |
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.
workspaceSymbolreturns nothing in this tooling — pyright only indexes open files, and the LSP tool doesn't expose the query parameter. UseGrepfor project-wide symbol search instead (e.g.,Grep "def format_md_table").goToImplementationis not implemented by pyright — usegoToDefinitioninstead.goToDefinitionon external-library imports returns "no definition found" — usehoverinstead, which gives you the class signature + docstring.- If
hoverreturnsUnknownfor a project-external import (e.g., pydantic, fastmcp), the langserver is stale — flag to the user that Claude Code needs a restart to re-readpyrightconfig.json. All project-internal imports should always resolve.
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:
-
Check pyright CLI on the affected file —
uv run pyright <path>. Pyright CLI is the source of truth; the LSP can be stale (especially after generator regen, stash/pop, or rebase). -
If pyright CLI agrees, fix it. Don't push code with pending pyright errors — CI will fail.
-
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.
-
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. -
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 workspacerootUriduring 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).
Monorepo with 3 packages:
katana_public_api_client/- Python client with transport-layer resiliencekatana_mcp_server/- MCP server for AI assistantspackages/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.
| 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)
feat(client): add feature # Client MINOR release
fix(mcp): fix bug # MCP PATCH release
docs: update README # No releaseUse ! for breaking changes: feat(client)!: breaking change
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).
| 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 |
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-modernizer → verifier → pr-preparer →
code-reviewer → /open-pr.
Configured in .claude/settings.local.json (gitignored):
- PostToolUse / Edit|Write|MultiEdit — runs
uv run poe fixsilently 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 retroto capture learnings. (When the harness-kit plugin is installed, also consider/session-retro— see harness-kit#30 — for project-side learnings.)
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.
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 |