Project Overview
Code Conventions
The backend is Kotlin on Spring WebFlux, the Studio TypeScript on Next.js. This page collects the rules both follow, the traps that made the rules necessary, and the checks that enforce them. How to build, run and test is in Development Setup.
Kotlin Formatting
KtLint 1.7.1 checks the Kotlin sources against the rules in .editorconfig: four-space indentation,
lines up to 160 characters, and wildcard imports only for the packages listed there. A root-project task
lints exactly the files you pass it:
./gradlew ktlintGitFilterCheck -PinternalKtlintGitFilter="backend/src/main/kotlin/ai/promethist/platform/service/AgentService.kt"
KtLint checks whole files, so touching a file means fixing any violation already in it.
-
The merge-request pipeline runs the same task on the Kotlin files an MR changes (
gradle:checkFormatting, allowed to fail). -
./gradlew ktlintCopyPrePushHookinstalls the repository’spre-pushscript as a git hook; it lints the staged Kotlin files and stops the push on a violation.
Beans and Component Scanning
@SpringBootApplication on PlatformApplication scans ai.promethist.platform and all of its
sub-packages, so a bean anywhere in it is registered. PlatformConfiguration adds a second
@ComponentScan of ai.promethist.**.{configuration,controller,component,service,repository} — the
scan that registers the services, repositories and configurations of promethist-common — and enables
the reactive MongoDB repositories of ai.promethist.**.repository.
That second scan has no test-type exclusion. Test classes in a package named configuration,
controller, component, service or repository are therefore registered too: a nested
@TestConfiguration in such a package becomes part of every @SpringBootTest context and clashes with
the real beans. Tests that need their own configuration live elsewhere, for example
AgentEndpointAuthorizationTest
in the security package.
Persistence
-
Project assets are polymorphic values embedded in the project document and are read back through hand-written Jackson converters in
configuration/MongoConfig.kt. A new polymorphic type, or a type that nests another polymorphic value, needs its converter adjusted in the same change — otherwise the write succeeds and the next read of the project fails (Asset Specifications). -
Collections the engine reads have their models in
promethist-common. A new field there is apromethist-commonrelease and a version bump ingradle/libs.versions.toml, not a platform-only change. -
Indexes the platform’s reads depend on — also on collections the engine owns — are ensured by startup migrations in
component/(*IndexMigration) when the application is ready. An index that already exists is left as it is, and a failure only costs a warning and the slower query.
Backend Tests
-
backend/src/test/resources/application.propertiesreplaces the main properties file in tests. A new required property —@Valuewithout a default, or a mandatory@ConfigurationPropertiesvalue — has to be added there as well, or every test that starts a Spring context fails. -
Mocking Kotlin non-null parameters with Mockito needs the helpers in
backend/src/test/kotlin/ai/promethist/platform/MockitoMatchers.kt(anyValue(),eqValue(),captureValue()); the plain Mockito matchers returnnulland fail at the call site. -
A test that needs its own Spring configuration keeps it out of the scanned package names, see Beans and Component Scanning.
MCP Tools and Guides
The hosted MCP server’s tool surface is a contract that tests enforce: snake_case argument names,
descriptions of at most 420 characters, a confirm argument exactly on the destructive tools, the
REST controller method as the guard, McpToolException for what the model may read, and size budgets
for the tool listing and the guides. The procedure is in
Adding a Tool, the enforced rules in
Contracts Enforced by Tests.
When a tool’s behavior changes, the operating guide that describes it changes in the same commit. The AI Assistant has its own tool classes and prompts (Adding a Tool); a behavior change usually belongs in both.
Studio
Components and Styles
-
Build UI from the existing components in
ui/andcomponents/and the established patterns instead of new styles:ToggleCardgrids for option tiles,SelectionGlowEffectfor selectable cards, the amber warning card ofcomponents/account-warning.tsx. -
When exactly one tile can be chosen, use
ToggleCardRadiocards in aToggleCardGroup(radio dot, arrow keys); keepToggleCard(check box) for options that switch on and off independently. -
A table row carries a single status tag. Further states go into tabs, filters or the detail view rather than a second tag in the row.
Navigation and Lists
-
In editor surfaces, link with
GuardedLink(components/guarded-link.tsx), notnext/link. OnlyGuardedLinkgoes through the navigation guard that asks before leaving a form with unsaved changes. -
Studio lists keep their page, page size, sorting, search, filters and scroll position per browser tab with
useListViewState(hooks/use-list-view-state.ts, which usesuseScrollRestoration), so that opening an item and coming back lands on the same place. Each list registers its key inlistViewKeys; its table setsautoResetPageIndex: false; a flow that redirects to a list and should open it fresh callsforgetListView. -
Pickers and
@-mention search filter withfilterBySearchfromlib/fuzzy-search.ts: case, diacritics and word order do not matter, and longer words may carry a typo.
Page Titles
The root layout sets the title template %s | PromethistAI, so a page or layout exports only its bare
title (Agents). A client-rendered page cannot export metadata; it gets a metadata-only layout.tsx
next to it. A layout whose title is a plain string cuts the template for the segments below it, so
titled layouts are not nested.
Data and Forms
-
Application data goes through TanStack Query with server-side prefetch and hydration (Rendering and Data, which also explains the next two rules).
-
An edit form seeded from a hydrated query writes the mutation response into the cache and lets an untouched form adopt newer data, because existing cache entries are hydrated only after the first render.
-
A form in a dialog or an animated container has its own local
Suspenseboundary: the form fields are lazy components, and their first render would otherwise suspend the whole dialog. -
Integrations come in three kinds with separate edit views. Branch on the kind before opening, editing or deleting one by
ref(Project Integrations).
Formatting, Lint and Types
cd frontend
npm run typecheck # tsc --noEmit
npm run lint # eslint --fix
npm run format # prettier --write, with the Tailwind and import-sorting plugins
ci:lint, ci:format and ci:typecheck are the non-modifying variants. TypeScript runs in strict mode
with noUncheckedIndexedAccess; ESLint uses the Next.js configuration. Not every committed file is
Prettier-clean, so run prettier --check on a file before reformatting it as a whole, rather than
mixing a reformat into a functional change. TypeScript errors under .next/dev/types come from stale
development builds and can be ignored.
The pipeline lints and type-checks the Studio no further than next build does, and it does not run
the backend tests, so run these checks before pushing.
Documentation
A change that alters behavior described in this documentation or in the MCP operating guides
(backend/src/main/resources/mcp-guides/) updates them in the same commit. The rules for these pages —
structure, cross references, source links, diagrams and content — are in CLAUDE.md at the repository
root.
Branches and Commits
Work happens on branches named after the ClickUp task, PRP-{id}-{slug}; the pipeline gives their merge
requests an issue environment (Environments). Commit subjects
start with the task id: PRP-{id}: {task name}.