Reply Pilot Refactoring Context

This document contains durable context for active Reply Pilot refactoring targets. It follows the external refactoring-plan skill as a workflow guardrail. Project truth remains in AGENTS.md, local docs, source code, and tests.

Work item status lives in docs/refactoring-tracker.md.

Current Target

Target boundary:

  • reply-pilot-app is a replaceable browser-facing UI/BFF.
  • reply-pilot-app calls application state and business operations through reply-pilot-be HTTP/JSON APIs.
  • reply-pilot-app must not own direct PostgreSQL, Gmail, OpenAI, Jira, CME/CmD, lead-import, activity/contact/task/company persistence, or durable app-state storage.
  • reply-pilot-be owns DB access, integration orchestration, business workflows, and stable JSON contracts for the current Flask app and a future UI rewrite.
  • reply-pilot-be now runs as a Java 21 Spring Boot/Maven REST/JSON backend. The legacy Flask backend package, Python backend tests, Python-only dependencies, and obsolete Python backend utilities have been removed.
  • reply-pilot-worker uses Java 21 and Spring ThreadPoolTaskScheduler, with separate @Component job classes, JDK HTTP client/server and Jackson JSON. Seven periodic jobs use @Scheduled(fixedDelay) measured from completion; Gmail watch and import use Spring triggers for their dynamic retry deadlines. Status/metrics and the HTTP status server are separate from job execution. Preserve all nine HTTP job flows, per-job non-overlap, Gmail retry/import state, env defaults, /healthz, /statusz, and the heartbeat JSON. It remains a separate single-instance service without DB or Google credentials. Python worker code is retired.
  • Search follows the same replaceable UI boundary: reply-pilot-app -> reply-pilot-be -> reply-pilot-search.
  • Email runtime data follows the Gmail ownership boundary: Google Gmail API/mailbox -> reply-pilot-google -> reply-pilot-be. reply-pilot-google owns technical OAuth material, Pub/Sub credentials, watch/rate-limit state, Gmail API calls, the account-scoped mailbox cache, and /api/reports/email-weekly-counts. It stores inbox and sent-only threads in the same account cache. reply-pilot-be uses internal HTTP through the canonical reply-pilot-google DNS name and is not the durable cache owner in gmail_service mode.

Accepted Google integration final state as of 2026-08-18:

  • Use reply-pilot-google as the sole Google integration and credential-owning module. The source/runtime rename, repository consumer switch, and repository Calendar ownership move are complete. The production Gmail and Calendar cutovers and the legacy naming cleanup are also complete.
  • Preserve the existing Gmail HTTP contracts, mailbox cache, watch/Pub/Sub behavior, technical Gmail credential, host port, and consumer-visible behavior. reply-pilot-google is the only supported module and internal DNS name.
  • The stable browser/runtime call direction is reply-pilot-app -> reply-pilot-be -> reply-pilot-google. Browser-facing modules and other internal consumers call narrow Gmail or Calendar HTTP APIs; they never receive Google access or refresh tokens.
  • reply-pilot-google owns Google OAuth authorization-code exchange, encrypted per-user Google grants, access-token refresh, revocation, Google API calls, Google-specific retry/quota handling, and the existing technical Gmail mailbox credential.
  • Keep the technical Gmail mailbox credential separate from per-user Google grants. The technical credential remains a SOPS-managed runtime artifact; the existing per-user Calendar grant remains encrypted in app_user_google_calendar_profile unless an implementation slice proves a schema change is necessary.
  • Only reply-pilot-google may decrypt or refresh per-user Google grants. reply-pilot-be continues to own Reply Pilot authentication, authorization, UI-facing orchestration, stable app-facing contracts, and persistence of the opaque encrypted Calendar credential envelope, but consumes Google capabilities over authenticated internal HTTP.
  • The Google Cloud project, OAuth branding/consent configuration, runtime module, OAuth client, technical Gmail credential, and per-user grant are distinct concepts. The migration must not merge credentials merely because they share one Google Cloud project.
  • Production runs only the canonical Google module and path. Rollback uses the current canonical deployment and SOPS-managed credentials rather than a parallel legacy-named container or stale mailbox-cache copy.

Accepted production IČO target state as of 2026-08-20:

  • reply-pilot-be has one validation and ownership path for manual company create/edit. It removes whitespace, validates new or changed Czech IČO as eight ASCII digits, and writes only the COMPANY_REGISTRATION_NUMBER identifier. The public JSON/form property remains company_registration_number.
  • Authoritative IČO claims use the existing global identifier uniqueness and fail the whole mutation with a field-level conflict when another party wins ownership, including concurrent create/update races. They do not use ON CONFLICT DO NOTHING.
  • Lead import and CME synchronization use the same optional-value policy and target ownership operation. Malformed or incompatible source values remain in their audit record with an explicit error; they do not become canonical identifiers. CME isolates such errors with a row savepoint so other rows in the same synchronization can continue.
  • Lead and CME IČO matching use only the target identifier. A valid imported value may fill an empty target; an existing different authoritative value is not overwritten.
  • Company merge checks both locked parties before moving related data. Zero or one distinct authoritative IČO is deterministic; two different authoritative IČOs reject and roll back the merge.
  • Expand changeset 0058 safely backfills valid unambiguous legacy Czech IČO, moves every unresolved legacy value into audit-only identifier type LEGACY_UNRESOLVED_COMPANY_REGISTRATION_NUMBER, and enforces canonical format plus at most one canonical IČO per party. Its scoped normalized value prevents quarantine rows from becoming matching or ownership keys.
  • Backend, search, reports, import, merge, and CME synchronization read and write only party_identifier.
  • The canonical Czech scheme remains COMPANY_REGISTRATION_NUMBER, with the normalized value restricted to eight ASCII digits. Verified foreign commercial-register entries use COMMERCIAL_REGISTER_NUMBER; their normalized value includes country, issuing register, section, and number so the existing global uniqueness has a complete issuer scope. DIČ and other identifier families remain separate.
  • Expand changeset 0058 and targeted contract changeset 0059 are applied in production. Before the drop, drift was zero. After it, the legacy column, trigger, and function are absent; 2,964 canonical rows and 573 raw-preserved quarantine rows remain, with zero invalid canonical values, duplicate canonical values, or parties with multiple canonical values.
  • A verified pre-contract logical backup is retained. Company list, detail, and search smoke checks returned HTTP 200, services remained healthy, and three post-contract CME runs linked 2,669 source rows without creating a company.

Accepted VAT expand target for RP-4122:

  • vat_jurisdiction is the versioned jurisdiction/country/local-name catalog; its explicit exceptional mappings are EL -> GR, XI -> GB, and Czech local_name = DIČ.
  • party_vat_registration keeps raw and normalized values, allows at most one row per party and jurisdiction, and deliberately has no global uniqueness on normalized VAT value.
  • the shared backend syntax policy uppercases first, removes only whitespace, -, ., and /, then rejects any remaining unsupported characters, unknown prefixes, or jurisdiction-invalid formats. Syntax validation does not call live VIES.
  • expand item 34.1 is additive: company payloads retain tax_identifier and add vat_registrations[]; the authenticated catalog API is read-only.
  • compatibility item 34.2 routes valid manual create/edit, lead promotion, and CME promotion through one policy and transactionally mirrors raw scalar plus normalized jurisdiction row. VAT equality is not ownership, a save conflict, or an automatic lead/CME match. Same-jurisdiction merge disagreement stops before related data moves.
  • inventory/backfill item 34.3 is a one-off backend command that dry-runs by default, requires --apply, classifies every non-empty organization scalar, applies the refined merged-party rule, and refuses unexplained target drift; after item 34.5 it no longer queries legacy TAX_IDENTIFIER rows.
  • legacy identifier cutoff item 34.5 filters those rows from backend/app/search, removes the manual supplier-identifier option, moves wholesale VAT lookup to party_vat_registration, and stages reversible contract changeset 0068. Immutable supplier evidence and AI extraction classifications remain explicitly non-authoritative.
  • production item 34.6 deployed those consumers, recovery-tested backup reply-pilot-20260821T105254Z.sql.gz, and applied only 0068. Its audit snapshots all 318 deleted legacy rows and records 318 -> 0; the zero count held after a company write and scheduled CME jobs. The 1,138 non-empty organization scalars, 1,067 VAT registrations, and scalar column were preserved.
  • backend cutover item 34.7 makes the collection authoritative for company create/edit/list/detail/search, lead and CME promotion, merge, and AI draft context. The temporary scalar JSON adapter is derived from zero or one collection row, rejects scalar mutation when multiple rows exist, and never reads or writes party_organization.tax_identifier. Per-entry mutation errors retain their collection index. Equal normalized values on separate effective parties remain legal and never select an owner.
  • browser/search/batch cutover item 34.8 edits and renders the complete collection from the authenticated jurisdiction catalog, preserves submitted row order and indexed errors, indexes every registration, and treats VAT in wholesale matching only as corroboration for an existing non-VAT match.
  • production cutover item 34.9 deployed backend, search, and app in dependency order, completed a full company reindex, accepted the authenticated form and a data-preserving company save, and verified an existing equal VAT value on two active parties remains non-unique. No production party was given a second registration and no lead, AI, or merge business workflow was manufactured without an approved target; those paths retain deterministic test evidence.
  • scalar-removal item 34.10 deletes the temporary JSON/form/DTO adapter and the completed one-off backfill command, leaving no organization-scalar runtime consumer. Contract changeset 0069 is staged behind contextFilter="contract" with reconciliation preconditions and a scalar-restoring rollback. Production item 34.11 recovery-tested a fresh backup and applied only 0069; the party, organization, and VAT counts remained 4929 / 3895 / 1068, the scalar column is gone, and unrelated contracts 0053 and 0054 remain pending. Source snapshots and immutable evidence remain non-authoritative.

Accepted email-thread/company resolution target for RP-4121:

  • reply-pilot-be owns one canonical dynamic resolver over normalized activity data. reply-pilot-app consumes its HTTP contracts and does not independently merge company-name mentions, activity-party links, or contact matches.
  • The resolver collects deduplicated external addresses from from, to, cc, and bcc across every message in a thread. A visible company matches only by an exact active company email contact, an exact active email contact of a person currently linked through CONTACT_FOR, or an exact company-owned DOMAIN. It supports zero, one, or multiple companies per thread.
  • A DOMAIN is an explicit manual assertion of company ownership. Email ingest and task creation never infer or create it, technical ownership verification is not part of this target, and no runtime domain/address blacklist is added. Existing invalid shared-provider DOMAIN rows identified by the production analysis are deleted before domain matching becomes canonical.
  • Unknown addresses do not automatically create a person or company. They are presented on an inbox-adjacent page grouped by normalized address, with thread count, last occurrence, and links to the affected threads. Resolution reuses the existing person, company, contact, and CONTACT_FOR mutations.
  • Marking a thread reviewed without assignment is durable per thread, not per address. A later unresolved thread from the same address must appear again.
  • Company-name text matching and primary/participant activity links alone are not canonical company signals. Task-to-thread linkage remains independent of the derived company set.
  • All 25 legacy explicit links were resolved on 2026-08-21: three were discarded by owner decision and 22 were independently reproduced by canonical rules before their rows were removed. email_thread_company_link is empty and no replacement override model is planned.
  • Reconciliation verifies dynamic resolver results and cross-screen parity; it must not recreate a materialized thread/company mapping table. Contract changeset 0054 remains a later production step after the retained 25 audit events have an explicit delete-or-archive decision.

Current verified state as of 2026-06-13:

  • Standard non-test app startup requires BACKEND_API_BASE_URL.
  • App prompts, reply drafts, app user profile, app configuration, Jira assignee mapping, simple-auth nonce persistence, read models, and planned mutation slices are backend-backed in standard runtime.
  • App views.py is route-map-like and delegates to workflow modules.
  • reply-pilot-app no longer has direct PostgreSQL-capable production store code, no longer builds a production task_store extension, no longer exposes REPLY_PILOT_DB_* runtime config, and no longer depends on psycopg.
  • Test-only in-memory stores remain in the app package to preserve app workflow tests without backend or database services.
  • App-local Gmail/OpenAI implementation fallback was removed from standard runtime and app package code; Gmail/OpenAI calls are backend-owned.
  • Direct app-to-search runtime access was removed; app search UI calls backend /api/search, and backend calls reply-pilot-search.
  • reply-pilot-be has a Java Spring Boot/Maven implementation track with /healthz, metadata, AI prompt, reply draft, app user profile, app configuration, Jira assignee, simple-auth nonce, and search proxy endpoint coverage, plus company/person and activity/contact/email-thread read-model endpoint coverage, plus task read-model endpoint coverage, plus company create/update/merge/visibility and requirement-review mutation endpoint coverage, plus person, party contact, company identifier, and company-person unlink mutation endpoint coverage, plus task mutation, Jira proxy/task-sync, AI draft generation, inbox/email/attachment read, mailbox watch/import, email draft/send, lead import, and worker job endpoint coverage, validated against the current backend contract. Java now has concrete JDBC worker beans for CME company sync, AI requirement classification, requirement aggregation, requirement monitoring/report, and historical requirement backfill.
  • Backend tests are maintained in the Java/Maven/JUnit 5 test suite under reply-pilot-be/src/test/java. There is no active Python backend test suite.
  • Java has a production JDBC ActivityEmailImportRepository for base activity-email persistence: activity, activity_email, activity_participant, activity_email_attachment, and activity_email_link. It also ports deterministic incoming-email extracted facts/evidence into party_feed_fact, party_supplier_identifier_fact, party_requirement_evidence, and party_requirement_eval_queue, and transitions already linked email_thread_reply Jira tasks to Drafting Reply for newly imported incoming external replies. For unlinked incoming supplier replies with one resolved supplier company, Java also creates the automatic Email Thread Reply task after successful email import commit and uses the documented default reply-assignee priority.
  • Java lead import now persists sent outbound email activity through the caller-owned JDBC connection inside JdbcLeadImportRepository.markImported, so the sent email activity, recipient contact link, lead item update, activity note, and batch progress refresh share one DB transaction. Gmail send and Jira issue creation remain external side effects outside DB rollback semantics.
  • Backend Jira task sync/read models now include delegated Reply Pilot Task issues: sync JQL includes the configured issue type, Jira reads customfield_10269 by default, local cache stores the delegated task type in task_jira_reply_pilot_task.task_type_value, task APIs expose task_type_value, and default task list reads hide Done delegated tasks unless the request explicitly filters by status or Jira key.
  • Backend read-model company/task endpoints apply a task-derived read-only visibility exception: the local assignee or mapped Jira assignee of any active Jira task can read only that linked company and active task. Company collaboration remains limited to active assigned reply_pilot_task items; unrelated companies, unassigned tasks, and Done tasks stay inaccessible.
  • Backend task mutation endpoints now support delegated Reply Pilot Task resolve/close actions. Resolve is limited to the cached Jira assignee, stores the entered text in Jira description, reassigns the task to the locally stored requester, and keeps In Progress; close is limited to the requester and transitions the Jira task to Done without changing assignee.
  • App task views now support delegated Reply Pilot Task issues. The app keeps reply_pilot_task as its own Jira work type, carries backend-provided task_type_value, renders delegated task detail/resolve pages with linked company, Jira description, and read-only Jira comments, and calls backend resolve/close mutations instead of Jira directly.
  • Search now indexes delegated Reply Pilot Task issues, including task_type_value, and includes active delegated tasks in assigned-scope task search for the cached assignee. Done delegated tasks stay indexed for explicit search through normal company scope or company.view_all, but they do not grant the active delegated-assignee search exception.
  • reply-pilot-be Docker/Compose runtime is configured to build and run the Java 21 Spring Boot jar. Docker daemon validation is tracked in item 26.1. Python Flask backend route-map cleanup and legacy Python backend code removal are no longer active migration targets.
  • Current verified state as of 2026-08-18: Gmail mailbox data is owned by reply-pilot-google. The Google module writes account-scoped cache files under data/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/, exposes internal cache endpoints for BE reads, computes GET /api/reports/email-weekly-counts?weeks=104&include_current=true from that Gmail-owned cache/runtime data, and both reply-pilot-be and reply-pilot-jira-reports use the canonical http://reply-pilot-google:5000 endpoint. Production now runs the canonical Google container, path, image and DNS alias with no legacy-named runtime compatibility. In EMAIL_SYNC_BACKEND=gmail_service mode, reply-pilot-be reads the mailbox cache, history cursor, attachment metadata, and attachment bytes only through Gmail-module HTTP.
  • Current verified state as of 2026-08-18: Calendar OAuth exchange, refresh, revocation, credential encryption/decryption, and public Google Calendar API calls live under reply-pilot-google. reply-pilot-be keeps its existing app-facing Calendar routes and database table, but treats the stored grant as an opaque encrypted envelope and sends it to the Google module over internal HTTP. BE uses shared GOOGLE_API_BASE_URL and GOOGLE_API_TIMEOUT_SECONDS settings for Gmail and Calendar module calls, and authenticates Calendar-only internal endpoints with GOOGLE_API_TOKEN; Google-specific mailbox configuration such as GMAIL_TOKEN_FILE remains Gmail-prefixed. Local and production Calendar OAuth values are configured in the Google SOPS env while BE has none of the Calendar client or encryption secrets. Production has active v1 credential envelopes updated through the canonical Google module.
  • Current verified state as of 2026-07-10: the Gmail cache can be extended with sent-only threads without adding a second cache or worker. Full sync performs an inbox pass followed by an in:sent pass with skip_cached_threads=true, then imports the unified cache into the activity model. Operators can trigger the same sent-only pass through POST /api/mailbox/sync/sent.
  • Current verified state as of 2026-07-30: all production Jira requests caused by an authenticated browser action use the same app user's delegated 3LO access. The exhaustive call-site audit leaves only Jira task sync, automatic incoming-email Jira operations, and Jira reports on technical credentials. The authorization request now includes read:jira-user, read:jira-work, write:jira-work, and offline_access. Existing grants need a new consent before they receive the added read scope. After re-consent, RP-3509 confirmed the human actor as creator, reporter, comment and attachment author, and change-history author for unassign, reassignment, summary, description, and transition to Done. A no-header Jira task-sync smoke also confirmed that the technical background path remains operational.

Planned delegated Jira task workflow target:

  • Add support for Jira issue type Reply Pilot Task for delegated tasks. The Jira admin edit URL is https://internet-handel.atlassian.net/secure/admin/EditIssueType!default.jspa?id=10206.
  • Delegated Jira tasks use Jira status values In Progress and Done. In Progress tasks remain visible/actionable; Done tasks are hidden from the default task overview only. They may still be reachable through other explicit task lookup paths when normal authorization allows it.
  • Delegated Jira tasks carry a Jira custom field taskType with dropdown values. The Jira custom field id is customfield_10269; the confirmed option labels are General, Meeting organization, and Registration (B2B).
  • Delegated task creation can choose an assignee immediately. Jira remains the source of truth for assignee, status, task type, summary, description, and comments; Reply Pilot stores only the local cache and workflow links it needs.
  • Each delegated task links to one company. Jira description must contain a human link to the company, but Reply Pilot's database value is the source of truth for workflow and authorization. A user assigned to an active delegated task may read that linked company even when normal company scope would not allow it. That exception must not grant access to any other company, to unassigned tasks, or to companies linked only to Done delegated tasks.
  • The task requester is the app user who created the delegated task in Reply Pilot. Jira does not currently hold this value, so Reply Pilot must store the requester in its own task cache/model.
  • Delegated tasks appear in the existing task views like other Jira tasks.
  • The assignee can open a resolve page from the task, enter text, and send the task back to the requester while keeping Jira status In Progress. The entered text is stored as part of the Jira ticket description.
  • The requester can close the task, moving Jira status to Done without changing the current assignee.
  • The delegated task detail/resolve page shows the linked company and existing Jira comments. Comments remain read-only in Reply Pilot and should use the existing Jira comment display component already used by the current task UI.

Planned user-delegated Jira execution target as of 2026-07-30:

  • Every Jira API call caused by an authenticated user's browser action must use that app user's Atlassian 3LO access token. This includes issue creation, update, assignment, unassignment, transition, comments, attachments, Jira user lookup, issue/status/comment reads, searches, and explicit refreshes.
  • An interactive Jira operation must never silently fall back to the technical Jira account. Missing, expired, invalid, undecryptable, or unusable delegated credentials return HTTP 428 with code jira_oauth_required; the app keeps entered form data and offers the existing Jira connection flow.
  • The effective app_user_id comes from backend-authenticated UserAuthorization, never from an assignee, requester, operator, or arbitrary user id supplied in a request payload.
  • Every Jira call inside one interactive workflow uses the same authenticated actor. A delegated comment followed by assignment or transition must not mix the user token and technical-account credentials.
  • Keep one JiraIssueClient implementation. Reuse JiraOAuthAccessProvider, the existing token refresh behavior, and the delegated api.atlassian.com/ex/jira/{cloudId} request path; do not add a second parallel Jira client or ambient thread-local user context.
  • Transport item 30.1 is implemented: every supported JiraIssueClient operation has an explicit actor-aware Bearer variant, including multipart attachment upload and binary download, while no-actor variants retain the technical-account path. Items 30.3 through 30.7 use those variants for all browser-triggered Jira call sites.
  • Error-contract item 30.2 is implemented: one backend controller advice maps every JiraOAuthRequiredException to HTTP 428 with jira_oauth_required; Jira-related app HTTP clients expose one shared recoverable exception, and affected forms preserve submitted values while offering the existing Jira connection flow.
  • Task-workflow item 30.3 is implemented: Supplier Onboarding reply, Reply Pilot resolve/close, Email Thread Reply update/close, and move-to-waiting use one backend-authenticated actor for every Jira call in the workflow. Payload assignee/requester ids do not select credentials, Supplier reply author identity comes from the authenticated app user, and existing partial-success warnings remain in place after irreversible comment/email side effects.
  • Task/company mutation item 30.4 is implemented: company reassignment, Supplier Onboarding creation/edit, Reply Pilot Task creation, task reassignment, and manual Email Thread Reply task creation use the backend-authenticated app user's OAuth token for every Jira lookup, read, create, update, and assignment in the workflow. Assignee and requester ids remain business data and do not select credentials; the app handles the shared OAuth-required contract and offers the existing Jira connection flow.
  • Attachment item 30.5 is implemented: attachment settings/listing, streamed upload, same-filename replacement deletion, rollback deletion, metadata verification, and binary download all use the backend-authenticated app user's OAuth token. One replacement attempt keeps the same actor throughout, OAuth failure never falls back to the technical account, and the app offers the existing Jira connection flow for list, upload, and download failures.
  • Lead-import/generic-mutation item 30.6 is implemented: lead import derives the operator id from backend-authenticated UserAuthorization and uses that actor for Jira user lookup, create, transition, and update. Generic Jira create/update/transition/comment/unassign routes require the same authenticated context and pass its actor to Jira; request-body operator_user_id remains compatibility data and cannot select credentials. Missing or invalid OAuth keeps the shared 428 contract, and the lead-import page preserves submitted form values while offering the Jira connection flow.
  • Remaining-read item 30.7 is implemented: Jira proxy myself, user lookup, issue/search/assigned/stale/detail/transition/comment reads and explicit task refresh require the backend-authenticated app user and use that actor's OAuth token. App read workflows retain available local data when Jira requires OAuth and offer one deduplicated Jira connection prompt; the global task badge omits its remote count instead of failing the page.
  • Existing technical-account Jira methods remain available only for operations without a human actor: Jira task synchronization, automatic incoming-email task creation or transition, and Jira reporting reads. These callers form an explicit allowlist that was re-audited and accepted in items 30.7 and 30.8.
  • JIRA_EMAIL/JIRA_API_TOKEN remain configured while the background allowlist exists. The migration does not imply removing the technical integration.
  • Reuse the encrypted credential envelope already stored in app_user_jira_profile.jira_oauth_credentials_ciphertext; no additional DB schema is expected for this migration.
  • Existing Reply Pilot transition historyMetadata may remain as supplemental workflow/reporting metadata, but it is not user impersonation. The Bearer token must determine the real Jira actor.
  • Live Jira acceptance must verify creator, reporter, comment author, attachment author, and change-history actor separately. Do not claim reporter attribution from OAuth authentication alone until Jira behavior is observed.

Source Of Truth

Use these project docs and code areas before planning or executing refactor items:

  • AGENTS.md
  • docs/refactoring-context.md
  • docs/refactoring-tracker.md
  • docs/jira-oauth.md
  • docs/module-boundaries.md
  • docs/web-app.md
  • docs/backend.md
  • docs/integrace.md
  • docs/architecture-reporting.md
  • docs/container-runtime-contract.md
  • docs/remote-server.md
  • docs/database.md
  • docs/activity-model.md
  • docs/permissions.md
  • reply-pilot-google/ as the current Google/Gmail implementation
  • reply-pilot-be/src/main/java/cz/replypilot/be/controller/GoogleCalendarController.java
  • reply-pilot-be/src/main/java/cz/replypilot/be/service/GoogleCalendarService.java
  • reply-pilot-google/src/main/java/cz/replypilot/google/calendar/GoogleCalendarController.java
  • reply-pilot-google/src/main/java/cz/replypilot/google/calendar/GoogleCalendarService.java
  • reply-pilot-google/src/main/java/cz/replypilot/google/calendar/GoogleCalendarSettings.java
  • reply-pilot-db/migrations/0049_create_app_user_google_calendar_profile.sql
  • source code and tests in reply-pilot-app/ and reply-pilot-be/
  • task and Jira workflow code in reply-pilot-app/reply_pilot_app/workflows/, reply-pilot-app/reply_pilot_app/jira_work_types.py, reply-pilot-be/src/main/java/cz/replypilot/be/controller/, reply-pilot-be/src/main/java/cz/replypilot/be/service/, and reply-pilot-be/src/main/java/cz/replypilot/be/adapter/db/
  • Jira integration contracts and implementation in reply-pilot-be/src/main/java/cz/replypilot/be/domain/JiraIssueClient.java, reply-pilot-be/src/main/java/cz/replypilot/be/adapter/http/HttpJiraIssueClient.java, reply-pilot-be/src/main/java/cz/replypilot/be/service/JiraOAuthService.java, and their focused backend/app tests

Expected generic-skill docs missing in this repository:

  • docs/development/code-quality-charter.md
  • docs/project-layout.md
  • docs/auth-contract.md
  • docs/data-storage.md

Do not invent their contents. Use the existing project docs above until those documents are intentionally added.

Guardrails

  • Preserve existing app routes, templates, redirects, public API contracts, and user-visible behavior unless a tracker item explicitly changes them.
  • Do not deploy, commit, edit encrypted secrets, or change DB schema unless a tracker item explicitly requires it.
  • Keep Flask views.py modules route-map-like where project rules require that shape; this currently applies to browser-facing Flask modules such as reply-pilot-app, not to reply-pilot-be.
  • Do not reintroduce a Python backend implementation or Python backend test harness for reply-pilot-be; backend behavior belongs in Java code and Maven/JUnit validation.
  • Keep reply-pilot-app as BFF/UI. Integration orchestration, persistence, and reusable business operations should live in reply-pilot-be.
  • Do not add direct reply-pilot-app -> reply-pilot-search runtime calls.
  • For Docker, runtime, remote server, DB schema, activity/contact model, or UI changes, read the matching project docs listed in AGENTS.md before editing.
  • Delegated task company visibility must be a narrow task-derived exception: active Jira assignment to the current user may grant read access to only the linked company for that active delegated task. Do not broaden company.view_assigned or company.view_all semantics to all task-linked companies.
  • Keep Jira as the source of truth for delegated task status, assignee, comments, description, and custom fields. Backend code owns Jira calls and local cache updates; the Flask app must call backend APIs instead of Jira directly.
  • Keep Reply Pilot as the source of truth for delegated-task requester and the linked company id used by workflow and authorization, even though the Jira description also carries a human-readable company link.
  • Never select delegated Jira credentials from a request-body user id. Resolve the actor through AuthorizationService and pass the authenticated app_user_id explicitly through controller, service, and Jira client calls.
  • Do not silently retry an interactive Jira call with technical credentials after delegated OAuth fails. Preserve HTTP 428 jira_oauth_required as the recoverable UI contract.
  • Keep technical-account Jira use limited to the documented background allowlist. Do not globally switch the default Jira request helper to Bearer authentication because background operations have no user token.
  • Any new permission, changed permission semantics, seeded role change, DB schema change, or UI workflow change must update the matching project docs in the same implementation slice.
  • Preserve the existing Gmail API, cache layout, watch/Pub/Sub behavior, account identifier, and technical token bytes during the module rename.
  • Do not let reply-pilot-be, reply-pilot-worker, reporting modules, or the browser app read or decrypt Google credential rows. They consume explicit internal capability APIs from reply-pilot-google.
  • A runtime contract rename must update AGENTS.md, docs/remote-server.md, and docs/container-runtime-contract.md together, plus module README, root orchestration, SOPS documentation, and architecture diagrams.

Validation Gates

  • python3 -m pytest reply-pilot-app/tests
  • mvn -f reply-pilot-be/pom.xml verify for backend migration slices
  • script/check-architecture.sh
  • mkdocs build --strict for documentation changes
  • DB migration validation from AGENTS.md only when DB schema changes
  • rp <diagram.plantuml> for changed PlantUML diagrams
  • focused delegated Jira tests in HttpJiraIssueClientTest, JiraOAuthServiceTest, TaskMutationServiceTest, TaskMutationControllerTest, TaskAttachmentServiceTest, TaskAttachmentControllerTest, LeadImportServiceTest, LeadImportControllerTest, and JiraProxyControllerTest
  • focused app tests for OAuth-required error mapping, form preservation, Jira connection links, task mutations, attachment flows, and lead import
  • mvn -f reply-pilot-google/pom.xml verify for Google module changes
  • focused and full backend validation for every Google HTTP client or Calendar ownership change
  • python3 -m pytest reply-pilot-jira-reports/tests and mvn -f reply-pilot-worker/pom.xml test when their Google/Gmail consumer contracts change
  • Docker build, non-root identity, healthcheck, internal DNS alias, Gmail cache status, watch status, and read-only weekly-report smoke validation for the renamed module
  • SOPS decrypt/extract verification for every renamed or changed local/prod Google module secret; never print credential values as validation evidence
  • production validation of container health, mailbox/cache status, Gmail read operation, Calendar status and the report consumer path after Google runtime changes