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 ktlintCopyPrePushHook installs the repository’s pre-push script 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 a promethist-common release and a version bump in gradle/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.properties replaces the main properties file in tests. A new required property — @Value without a default, or a mandatory @ConfigurationProperties value — 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 return null and 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/ and components/ and the established patterns instead of new styles: ToggleCard grids for option tiles, SelectionGlowEffect for selectable cards, the amber warning card of components/account-warning.tsx.

  • When exactly one tile can be chosen, use ToggleCardRadio cards in a ToggleCardGroup (radio dot, arrow keys); keep ToggleCard (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), not next/link. Only GuardedLink goes 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 uses useScrollRestoration), so that opening an item and coming back lands on the same place. Each list registers its key in listViewKeys; its table sets autoResetPageIndex: false; a flow that redirects to a list and should open it fresh calls forgetListView.

  • Pickers and @-mention search filter with filterBySearch from lib/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 Suspense boundary: 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}.