Guidance for AI coding agents in this repo. PHP backend in lib/, Behat integration tests in tests/integration/, unit tests in tests/php/. Vue 3 + TS frontend in src/, tests colocated as *.spec.js/*.spec.ts.
Comply with the AI Contribution Policy (disclosure, accountability, security, licensing, code quality, autonomous behavior) and Contribution Guidelines (testing, DCO, license headers, conventional commits, translations).
Always:
- Add an
Assisted-by: AGENT_NAME:MODEL_VERSIONtrailer to every AI-assisted commit. - Disclose AI tool use in every PR description.
- Keep PRs focused on one concern; no unrelated files or incidental refactors.
- Verify dependencies exist in real package registries.
- Tell the contributor when an action would violate the policy/guidelines — name the rule and the alternative; never silently proceed.
- Warn before a PR grows too large (approaching several thousand lines) and suggest a split.
- Recommend a discussion ticket before complex changes (multiple subsystems, architectural decisions, unclear approach).
Never:
- Open issues, submit PRs, post review comments, or send security reports autonomously — a human submits every contribution.
- Add
Signed-off-bytags (only the human certifies the DCO). - Submit unverified security reports; report verified ones via HackerOne, not GitHub issues.
- Write PR descriptions, review comments, or issue reports for the contributor — these must be in their own words.
- Fully automate resolution of
good first issue-style issues. - Submit unreviewed code — remove dead code, redundant logic, excessive comments, and unrelated changes first.
l10n/is generated from Transifex (fix(l10n): Update translations…commits). Never hand-edit translation files.
Backend: PHP 8.2+, Nextcloud server 35 (appinfo/info.xml). Stay portable across MariaDB, MySQL, PostgreSQL, SQLite, Oracle (migrations, query builder, tests).
- Setup:
composer i.lib/Vendor/is Mozart-bundled third-party code (cuyz/valinor,firebase/php-jwt) — never edit or analyze it. - Checks:
composer cs:fix,composer psalm,composer rector:fix,composer lint. - OpenAPI: docblocks/psalm types +
lib/ResponseDefinitions.phpfeedcomposer openapi(also regenerates TS types). Regenerate after adding/removing/renaming a route, changing a controller signature or its@param/@return/psalm-shape, changing a response shape, or adding/removing an HTTP status code. CI fails on a stale spec orsrc/types/openapi/.
Frontend: Vue 3.5 + TypeScript, built with rspack (no Vite). State: Pinia (src/stores/, target) and legacy Vuex 4 (src/store/, being phased out). UI components from @nextcloud/vue only.
- Setup:
npm ci. Build:npm run build(prod),npm run dev/npm run watch. - Checks:
npm run lint:fix,npm run stylelint:fix,npm run ts:check. - OCS/API types generated via
npm run ts:generate(also triggered bycomposer openapi) intosrc/types/openapi/— regenerate, don't hand-edit. - No compat mode, mixins,
mapGetters/$set/::v-deep— removed in the Vue 2→3 migration; don't reintroduce.
- PHP unit (
tests/php/):composer run test:unit. Single file:composer run test:unit -- tests/php/Service/AvatarServiceTest.php; filter:composer run test:unit -- --filter="testMethodName". tests/php/bootstrap.phprequires../../../../lib/base.php: the suite only runs when this repo lives at<nextcloud-server>/apps/spreed. If you cannot run a suite, say so — don't claim it passes.composer lint/composer psalmrun standalone.- Integration (Behat,
tests/integration/):run.shagainst a local server, orrun-docker.sh. - Frontend:
npm run test(vitest + @vue/test-utils v2; no jest); single file vianpm run test -- <path>.
Every new file needs an SPDX header. Use AGPL-3.0-or-later, never AGPL-3.0-only:
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/Adapt the comment style to the file type; files that can't carry a header (binary assets) go in REUSE.toml. CI enforces REUSE.
- Do not commit or push unless explicitly asked. Leave changes in the working dir, summarize, and suggest a commit message.
- Never commit/push to
main— use atype/issue-or-noid/short-descriptionbranch (e.g.fix/12345/handle-mcu-disconnect), not generated names likeagent-xxxx. - Follow Conventional Commits with a component scope:
feat(chat): …,fix(call): …,fix(api): …. - Backports happen via a
/backport to stable-X.Ycomment on the merged PR — don't cherry-pick manually.
From a 2026-06 tech-debt analysis. New code uses the modern pattern.
- DI: constructor injection with property promotion. Don't add
\OCP\Server::get()calls — existing ones are debt (the service-locator blocks inlib/Room.phpwheregetName()even writes to the DB; ~42Server::get()calls instantiating federation proxy controllers, 13 inChatController). - Data access: new persisted entities use
QBMapper+SnowflakeAwareEntityinlib/Model/(templates:ConversationTag,ScheduledMessage).Room/Participantare not Entities — hand-hydrated inManager::createRoomObject()from columns aliased inlib/Model/SelectHelper.php; adding a column means touching the migration,SelectHelper,Managerhydration, and the constructor in lockstep. Query builder only (no raw SQL outside migrations). Build queries outside loops viacreateParameter/setParameter; chunkIN ()witharray_chunk(…, IQueryBuilder::MAX_IN_PARAMETERS)for Oracle andarray_merge(...$results)once after the loop (seelib/Model/ThreadMapper.php). - Services: reads/lookups in
lib/Manager.php; writes/mutations + event dispatch inlib/Service/RoomService.php/ParticipantService.php— keep the split. New services inlib/Service/named*Service. Lib-rootGuestManager,MatterbridgeManager,lib/Chat/ChatManagerare legacy naming. Uselib/Federation/,RecordingService,BotService,lib/RoomPresets/as templates, not 2016-era core. - Errors: lookups throw domain exceptions (
RoomNotFoundException,ParticipantNotFoundExceptioninlib/Exceptions/). Idempotent setters may returnbool(false = no-op), e.g.RoomService::setPermissions(); don't returnnull/falsefor "not found". - Events: typed only (
dispatchTyped()), extending theA-prefixed bases inlib/Events/, withBefore*/*pairs for mutations; registered inlib/AppInfo/Application.php. No string events/hooks. - Controllers/API: OCS controllers needing room/participant context extend
AEnvironmentAwareOCSController(populated byInjectionMiddlewarevia#[RequireRoom]-style attributes inlib/Middleware/Attribute/); others extendOCSController. PHP attributes only (#[NoAdminRequired],#[PublicPage],#[BruteForceProtection],#[ApiRoute]) — never docblock annotations. Responses areDataResponsewith psalm shapes fromResponseDefinitions.php. - Config: settings go through the
lib/Config.phpfacade; register new app-config keys inlib/ConfigLexicon.php(the emerging registry). - Caching: cache prefixes belong in
lib/CachePrefix.php— don't add ad-hoc prefixes (drift:hpb_serverslackstalk/,Capabilities.phpuses raw'talk::'). - PHP style: strict comparisons,
?Typenullables,matchoverswitch,str_contains/str_starts_with,readonlywhere applicable, arrow functions,JSON_THROW_ON_ERRORon new json calls. Prefer native backed enums for new value sets (onlylib/RoomAttributes.php,lib/RoomPresets/Parameter.phpexist today); bitflags (Attendee::PERMISSIONS_*,Participant::FLAG_*) stay int constants.
- Components: new/rewritten SFCs use
<script setup lang="ts">withdefineProps<T>()/defineEmits<T>(). - State: new state in a Pinia setup-style TS store (
defineStore('x', () => {…}), seesrc/stores/actor.ts,token.ts). No new options-style or JS stores. Never add to the Vuex modules (conversationsStore.js,messagesStore.js,participantsStore.js) — migration targets; if forced, keep minimal and flag in the PR. Don't add new Pinia↔Vuex coupling. Instantiate stores lazily inside actions/setup (useXStore()), not at module level. Stores sync through reactivity, not the EventBus. - Services/composables: TypeScript with named function exports (convert JS ones when touched). Error split: services throw (bare axios, no try/catch, no UI); stores/composables catch and surface via
showError(t('spreed', …))from@nextcloud/dialogs— don't toast from services or swallow withconsole.debug. Use types fromsrc/types/index.ts. URLs viagenerateOcsUrl()with{token}placeholders — never concatenation, neverOC.linkTo(). Translations:t/nfrom@nextcloud/l10n, interpolate via placeholder objects; date/time viasrc/utils/formattedTime.ts(Intl) — no moment.js. - EventBus:
src/services/EventBus.ts(typed mitt) bridges the non-Vue signaling/call layer into Vue only — not for component-to-component UI coordination or store-to-store sync; register every new event in theEventstype.@nextcloud/event-busis only for cross-app server events. - Dialogs: declarative
<NcDialog>in the owning template is the target;spawnDialog()only without template context;NcModalis legacy. - Icons/loading:
vue-material-design-iconscomponents (noticon-*CSS classes);NcLoadingIcon(noticon-loading*divs). - Styling: scoped styles with Nextcloud CSS vars (
var(--color-*), defined inapps/theming/css/default.css); hardcoded colors only for brand exceptions. Avoid new:deep()overrides of@nextcloud/vueinternals — prefer props/slots or an upstream issue. Spacing/dimensions use the standard vars (calc(x * var(--default-grid-baseline))), no magic numbers. Follow the string-writing rules. - Call layer (WebRTC/signaling):
src/utils/webrtc/simplewebrtc/,webrtc/models/,signaling.js,EmitterMixin.jsare pre-Vue legacy — don't copy these patterns; use ES6 classes + async/await, withsrc/utils/media/pipeline/as the template. Don't add new manual model.on()/.off()subscriptions in components — prefer a composable wrapper that cleans up.
Roadmap, not new-code patterns. Don't extend these; extract/repair when touching nearby code.
Backend:
\OC_Util::tearDownFS()/setupFS()inlib/Chat/Parser/SystemMessage.php— last private-server-API usage; replace with a public per-user FS API.Room.phpservice locators — move name resolution / display-name / last-message loading intoManager/formatter; removes theRoom↔RoomServicecycle and a getter-with-DB-write.- Enum migration — convert constant groups (
Room::TYPE_*,Participant::OWNER/…,Attendee::ACTOR_*) to native backed enums, keeping->valueat OCS/DB boundaries. - Room/Participant hydration — introduce a mapper owning the
SelectHelper⇄constructor mapping now spread over three files. - Stale "temporary" code — 15×
FIXME Temporary solution for the Talk6 releaseinlib/Manager.php;Room::OBJECT_TYPE_PHONE_LEGACY(@deprecated) still used in 5 places (Notifier,AvatarService,RoomController,RoomService,RestrictStartingCalls). - God classes —
RoomController.php(~3.3k, 63 endpoints),ChatController.php(~2.6k),ParticipantService.php(~2.4k, aSessionServicewould split out),RoomService.php,Manager.php. Don't grow them. - Copy-paste
BackendNotifiers —Signaling/,Recording/,Federation/sharedoRequest()+retry+PHPUNIT_RUNboilerplate; extract a base. MatterbridgeManager— 16-branch elseif ingenerateConfig(),is_null()/strpos/substrclusters; oldest-style file.- Tests — inverted pyramid (~61 unit files vs huge Behat suite)
- Static analysis — psalm level 4 with
findUnusedCode="false"and ~200-line baseline; tightening surfaces hidden debt. json_encode/json_decode— ~164 of 193 calls lackJSON_THROW_ON_ERROR; fix opportunistically.- Loose comparison in
BackgroundJob/CheckCertificates.php(== null) andCommand/Signaling/VerifyKeys.php(!=).
Frontend:
- Vuex→Pinia — 3 modules ~4,200 lines (
conversationsStore.js,messagesStore.js,participantsStore.js), bidirectional coupling, duplicated reactions state. Finishing removes the non-atomic fan-out indeleteConversationand ~23useStore()components. - Deprecated WebRTC APIs —
pc.addStream()andaddstream/removestreamlisteners insimplewebrtc/peer.jsande2ee/encryption.js; migrate toaddTrack()/ontrack. - Legacy call layer (~4,800 lines) — ES6-classify
simplewebrtc/andwebrtc/models/, replaceEmitterMixin/WildEmitter withEventTarget, convertsignaling.js(~26 promise chains) to async/await. Dropswildemitter/util/mockconsole. crypto-js→Web Crypto — 8 files use it only for SHA (prepareTemporaryMessage.ts,messagesService.ts,stores/session.ts,store/participantsStore.js,AdminSettings/TurnServer.vue, …);crypto.subtle.digestis async.hark(unmaintained) —media/pipeline/SpeakingMonitor.js,composables/useDevices.js; replace with native AnalyserNode/AudioWorklet.- Options API components — 141 SFCs; migrate directory-by-directory. Self-contained starts: AdminSettings (15/16), BreakoutRoomsEditor (4/4), Dashboard (3/3).
- JS stragglers — 8 services, 7 composables, 4 Pinia stores; convert to TS when touched.
- EventBus overreach — UI-coordination events (
focus-message,scroll-chat-to-bottom) and store-sync listeners should move to reactivity/provide-inject; signaling fan-out stays. :deep()overrides — ~174 of@nextcloud/vueinternals; audit on each library bump, upstream what's useful.@matrix-org/olm— deprecated upstream for vodozemac; track forsrc/utils/e2ee/.- Misc —
icon-*CSS classes (~11 files),OC.linkTo()insrc/collections.js,cropperjsv1,base64-jsine2ee/encryption.js,vue-material-design-icons(421 imports — consider@mdi/js+NcIconSvgWrapper, low priority).