Skip to content

Agents-flow

A slice-by-slice rollout that adds chat tools, REST endpoints, a 4-step onboarding wizard at /agents-flow/onboarding, and a GitHub webhook consumer so a workspace can opt a project into continuous monitoring with a few clicks (or chat prompts) and have Logrite watch every subsequent PR.

Each slice ships behind a feature flag so partial rollouts never break the existing manual scan flow.

SliceWhat it shipsStatus
v1a–v1a.410 read-only chat tools (list_projects, list_templates, list_agents, list_my_scans, get_scan_key_findings, get_my_profile, list_my_generated_logs, list_project_generator_results, list_roles)✅ shipped
v1bcreate_workspace chat tool (first WRITE — wraps existing POST /api/v1/auth/workspace)✅ shipped
v1cstart_github_app_install chat tool + POST /api/v1/github/app/install/start + GET /api/v1/github/app/install/callback + org_workspace_map table✅ shipped
v1denable_monitoring chat tool + POST /api/v1/monitoring/enable + monitoring_contracts table + stub provisioner✅ shipped
v1everify_monitoring, initiate_scan, get_scan_report chat tools + GET /api/v1/monitoring/{id}/health + POST /api/v1/scan/initiate (stub orchestrator behind flag)✅ shipped
v1fget_scan_results endpoint fix (/api/v1/scan-results/{id}, was 404-ing on a non-existent /api/v1/scans/{id}), deterministic scan-summary + “my latest scan” routing with a not-found→list_my_scans pivot, and get_my_activity (“what have I done on the app” = scans + generations + sign-ins)✅ shipped
v1f4-step React onboarding wizard at /agents-flow/onboarding (consumes the same backend endpoints as the chat tools)✅ shipped
v1gGitHub pull_request event intake — logrite-webhook extension + POST /api/v1/monitoring/pr-event + GithubAppTokenService + stub mode-routing switch (log-only)✅ shipped
v1hComment-mode goes live — real GitUtil.postPrComment behind LOGRITE_PR_COMMENT_ENABLED✅ shipped
v1iPush-mode goes live — real fix-branch + sibling-PR behind LOGRITE_PR_PUSH_ENABLED✅ shipped
MethodPathShipped inDescription
POST/api/v1/github/app/install/startv1cBegin GitHub App install flow — returns install URL (or 503 when env vars blank)
GET/api/v1/github/app/install/callbackv1cOAuth-style callback from GitHub — binds installation_id to workspace_id
POST/api/v1/monitoring/enablev1dEnable monitoring contract for a project — body {projectId, templates[], mode}. Response includes appliedTemplates[] (templates saved to the contract), droppedTemplates[] (requests that couldn’t be resolved or seeded), and autoSeededTemplates[] (templates that had no per-project project_analyzer_rules_config row at request-time but were materialized from the global analyzer_rules_config defaults so monitoring could enable cleanly). Resolution order per requested template: (1) match an existing per-project config; (2) auto-seed a new per-project row from globals when the requested template maps to a known log_type (SOC2 → soc2_compliance, HIPAA → hipaa_compliance, etc.) and globals exist; (3) otherwise drop. When nothing resolves or seeds and the project still has at least one existing config, falls back to that (logType=general preferred). Still returns 400 (no_template_for_project) only when the project has zero configs AND nothing was seedable. Auto-seed shipped 2026-05-15.
GET/api/v1/monitoring/{projectId}/healthv1eRead monitoring contract state + computed issues
POST/api/v1/scan/initiatev1eInitiate scan — body {projectId, template?, templates?, branch?, prNumber?}. When templates[] has >1 entries the orchestrator merges every matched project_analyzer_rules_config row’s rules into ONE analyzer pass, returning appliedTemplates[] + appliedRuleCount and stamping them onto log_scan_result.run_details (multi-template merged scan, post-v1i 2026-05-15). template stays the back-compat hook for chat / single-framework callers; ignored when templates[] is present.
POST/api/v1/monitoring/pr-eventv1gReceive a GitHub pull_request event forwarded by logrite-webhook
POST/api/v1/github/app/install/sync-repospost-v1i (2026-05-18)Bulk-import all installed repos for a bound installation on demand. Body {installationId, workspaceId} → InstallationSyncResult. As of 2026-06-05 this is the ONLY path that imports all repos — completeInstall / bindInstallation no longer auto-sync on bind (see below).
POST/api/v1/github/app/install/start-for-repopost-v1i (2026-05-18)Mint an install URL pre-scoped to one repo. Body {workspaceId, repoFullName} → {installUrl, stateToken, workspaceId}. Used by the agents-flow merged picker’s OAuth-only click CTA so a user can install the App on one specific repo without re-picking from a list.
GET/api/v1/agents-flow/picker/{workspaceId}post-v1i (2026-05-18)Merged step-3 picker. Query ?installationId=…&oauthToken=…&oauthUserName=…. Returns a List<MergedPickerEntry> combining DB projects, App-installed repos, and OAuth-listed personal repos. Each entry carries a source discriminator (db/app/oauth) and a monitorable flag so the wizard can render badges and route clicks correctly. Backend gracefully degrades — missing installationId skips the app source, missing oauthToken skips the oauth source, DB is always returned.
POST/api/v1/agents-flow/ensure-projectpost-v1i (2026-05-18)Lazy-create a project row for a non-DB picker entry. Body {workspaceId, repoFullName, source, installationId?, oauthToken?, oauthUserName?} → {projectId, projectFullName}. Idempotent — re-calling for the same (workspaceId, repoFullName) returns the existing project id.

Request body:

{
"installationId": 555555,
"repoFullName": "Deployd-io/Logrite-AI",
"prNumber": 482,
"headSha": "abc123",
"action": "opened",
"sender": "victor"
}

Returns 200 with a status discriminator so the webhook and curl smoke tests can see what happened without parsing log output:

StatusWhen
DISABLEDLOGRITE_PR_EVENTS_ENABLED=false (default — safe-ship)
NO_WORKSPACENo org_workspace_map row matches installationId (see installation_id ↔ workspace cardinality)
NO_PROJECTWorkspace has no project matching repoFullName
NO_CONTRACTProject exists but enable_monitoring never ran
STUB_LOGGEDFull chain ran — last_webhook_at stamped, scan orchestrator called, mode arm logged (v1g log-only)
FAILEDException — detail in message

installation_id is many-to-one with workspaces. A single GitHub App install can legitimately power many Logrite workspaces — the same way a Vercel/Coolify GitHub install can power many projects. Prod evidence (2026-05-20): installation 132075896 (Deployd-io org) has rows for workspaces 1857 and 1959, both bound through the normal /install/start → /callback flow.

Three call sites previously used OrgWorkspaceMapRepository.findByInstallationId (returns Optional), which threw NonUniqueResultException → Spring 500 whenever multiple workspaces shared an install. As of 2026-05-20:

CallerLookup now usesSemantics
GET /api/v1/github/app/install/installations (wizard picker)findFirstByInstallationIdOrderByInstalledAtDescIdDescnewest-wins — bound and boundToWorkspaceId reflect the most-recently-installed workspace
POST /api/v1/github/app/install/bindfindByInstallationIdAndWorkspaceIdexact (install, workspace) pair — re-binding a shared install to a new workspace inserts a fresh row instead of overwriting an older workspace’s binding. Body {installationId, workspaceId, orgLogin, repoNamePrefix?}; see “Repo prefix per binding” below
POST /api/v1/monitoring/pr-eventfindByInstallationIdAndInstalledAtIsNotNullOrderByInstalledAtDescIdDescfan-out — every bound workspace whose prefix admits the repo (see below)

PR fan-out. A PR event goes to every workspace bound to the installation whose repoNamePrefix admits the repo, deduplicated by workspace. Pending rows (installedAt null) are ignored. The handler exchanges the installation token once and runs the scan body per target project; the response is the first target’s, plus projectIds listing every project scanned. Bails, in order: NO_WORKSPACE (no binding, or every binding’s prefix excludes the repo), NO_PROJECT (names the admitted workspaces: Workspaces [a, b] have no project matching …), NO_CONTRACT.

Repo prefix per binding. org_workspace_map.repo_name_prefix (repoNamePrefix on bind) is a case-insensitive prefix of the repo name after owner/. Blank clears it; absent leaves it unchanged; allowed characters [A-Za-z0-9._-], at most 100. Changing it on an existing binding needs the same GitHub-admin check (X-OAuth-Token) as a first bind. With a prefix set, /sync-repos and the picker skip other repos, and enrol, ensure-project, add-repo and branch listing answer 403: Repository <full> is outside this workspace's connection to installation <id> (repositories must start with '<prefix>'). In the UI: Settings → Workspace → Source control → Link existing installation, with the optional “Only repositories starting with” field.

Do not add a UNIQUE constraint on org_workspace_map.installation_id — it would prevent the legitimate multi-workspace install case.

The mode field: from dead data to live consumer

Section titled “The mode field: from dead data to live consumer”

monitoring_contracts.mode is one of comment or push. The roadmap:

Slicemode behaviour
post-v1d (today’s prod)Persisted in monitoring_contracts. No consumer.
post-v1fSame — wizard collects the value but it remains dead data.
post-v1gFirst live consumer. PR-event handler routes to a switch(mode) block — both arms are log-only (would post comment / would push fix-branch). last_webhook_at starts populating, which clears the no_webhook_received_yet issue surfaced by verify_monitoring.
post-v1hcomment arm makes a real GitHub REST call to post a PR comment when LOGRITE_PR_COMMENT_ENABLED=true. Uses the v1g installation token. New PrEventResponse.status values: COMMENT_POSTED / COMMENT_NO_TOKEN / COMMENT_FAILED. push arm stays log-only.
post-v1ipush arm pushes a fix-branch + opens a sibling PR via GatekeeperService.generateLogs (existing service, untouched signature) when LOGRITE_PR_PUSH_ENABLED=true. After a successful sibling PR, drops a courtesy comment on the original PR linking it. New PrEventResponse.status values: PUSH_INITIATED / PUSH_NO_TOKEN / PUSH_NO_SCAN (orchestrator returned no scanId) / PUSH_FAILED.
Env varDefaultEffect when true
LOGRITE_PROVISIONER_ENABLEDfalsev1d provisioner creates real OpenSearch index / dashboards / monitors (currently throws provisioner_real_not_implemented until OS-side templates land)
LOGRITE_SCAN_ORCHESTRATOR_ENABLEDfalsev1e /scan/initiate runs the real clone-at-ref → analyzer chain (verified through stage 4/6 locally)
LOGRITE_PR_EVENTS_ENABLEDfalsev1g — /monitoring/pr-event actually processes; webhook forwards events. With this off, both sides 200 silently.
LOGRITE_PR_COMMENT_ENABLEDfalsev1h — flip on to enable real PR comments via GitUtil.postPrComment (per-project gate is monitoring_contracts.mode='comment')
LOGRITE_PR_PUSH_ENABLEDfalsev1i — flip on to enable real fix-branch + sibling-PR via GatekeeperService.generateLogs (per-project gate is monitoring_contracts.mode='push')

Exchanges the App’s private key for a per-installation access token:

  1. Sign a short-lived (≤10 min) JWT with the App’s RSA private key (supports both PKCS#1 and PKCS#8 PEM formats — GitHub Apps download as PKCS#1 by default)
  2. POST https://api.github.com/app/installations/{id}/access_tokens with the JWT as bearer
  3. Cache the returned token until expires_at - 60s

v1g uses this observationally only — the PR-event handler calls it to prove the App credentials work, logs success/failure, and proceeds. v1h is the first slice that consumes the token (to post a comment).

Returns null (with a warning log) when github.app.id or github.app.private-key-path is blank — caller treats as a non-fatal “token not available” state.

The webhook server now branches on event type:

  • push → existing auto-deploy path (Logrite-UI / logrite-analyzer / Logrite-AI on main and dev)
  • pull_request (action in opened | synchronize | reopened) → forward to backend POST /api/v1/monitoring/pr-event
  • other actions → 200 silently

Config (env var wins over webhook.json):

SourceField / VarDefault
envLOGRITE_PR_EVENTS_ENABLEDunset
webhook.jsonpr_events_enabledfalse
envLOGRITE_BACKEND_URLunset
webhook.jsonbackend_urlhttp://localhost:8080

Forwarding is async + 5s timeout; failures log but don’t retry (GitHub re-delivers on the next PR sync).

It was a 3-step React component mirroring the chat tool chain:

  1. Pick or create workspace (defaults to Redux currentWorkSpace)

  2. Install the GitHub App (button calls /github/app/install/start and opens the GitHub install URL — shows yellow notice when 503 not configured)

  3. Monitor & scan — pick a project and its framework chips, with an optional scan.

    The same step renders the optional “Run a scan now” panel — branch dropdown populated from /api/v1/agents-flow/branches/{ws} (the live endpoint added 2026-05-19), with state.project.projects[].repository.branches as a secondary fallback for legacy db-source picks. The legacy fallback comes from GET /api/v1/project/{ws}/{email} — note that the /api/v1/user/userProfile payload no longer ships branches as of 2026-05-19 (perf fix — see “userProfile perf shrink” below). Leaving the dropdown blank falls back to the backend’s main default; the project’s last-used branch is auto-prefilled until the user touches it.

    Two action buttons in step 3:

    • Run scan (secondary) — fires POST /api/v1/scan/initiate only. The user stays on the wizard and reviews the response inline.
    • Run scan + Generate logs (primary) — fires the same scan, then navigates to /scan-results/:scanId with state: { startAnalyzer: true, autoApproveAndGenerate: true }. The scan-results page auto-chains analyzer → approve-all → submit → generator and never surfaces the AnalyzerSuccessScreen. While the auto-flow is mid-flight (analyzer done, generator not yet started) the page shows a “Approving findings and starting log generation…” loader so the Approve All / Generate Logs buttons don’t flash.

    The autoApproveAndGenerate flag is captured at mount in useScanState (location.state is wiped on mount by useScanResultsLogic) and treated as off on stale post-reload entries — a refresh can’t silently re-fire approve+generate against an old scanId. Auto-flow guarded by autoApprovedRef in useScanState to prevent double-fire.

    Orchestrator-already-ran short-circuit gate. useAnalyzerLogic probes GET /api/v1/scan-results/{id} and skips opening the analyzer WebSocket + POST /api/v1/analyzer when the persisted scan row already carries findings (combo flow: orchestrator ran AnalyzerService server- side before the UI mounted). This gate is keyed strictly on locationState.autoApproveAndGenerate — the normal “Begin Scan” path from Generator only sets startAnalyzer:true and lands on /generator/project/:id/scan where params.id is the project id; gating on startAnalyzer alone and probing by params.id lookup the project id as a scan id and silently skipped the normal-flow analyzer whenever a scan happened to share that numeric id. The probe target is also restricted to params.scanId || scanId so a project id is never sent as a scan-results path param. Fix shipped 2026-05-18.

Sidebar entry “Agents-flow” added in commit 77abc6ad.

After the agents-flow rollout switched analyzer WebSocket payloads from plain strings to AnalyzerMessageDTO JSON ({"milestone":"Analyzing","message":"END Scan ID:5407","messageType":"success"}), the frontend transport layer (utils/webSocketHelpers.js) still passed raw event.data for the analyzer path while the generator path JSON.parsed. The queue processor + detached handler then ran String.includes("Analyzing") against the stringified JSON — which matched on every message via the "milestone":"Analyzing" field — so the else-if chain short-circuited there and the END Scan ID: branch was never reached. UI symptoms: stuck on “Gathering Key Findings…” or flipped straight to AnalyzerSuccessScreen with scanId=#null. Fix parses the DTO at the transport boundary (analyzer path mirrors the generator path: JSON.parse when the payload starts with {, fall back to raw string for legacy plain-text emitters) and reroutes the END Scan ID: regex in useAnalyzerLogic to test the extracted text.

Installed-repos auto-sync + merged picker + redirect-bug fix (2026-05-18)

Section titled “Installed-repos auto-sync + merged picker + redirect-bug fix (2026-05-18)”

Three related changes ship together so the agents-flow wizard’s step-3 picker shows every repo the user could pick and the GitHub-back-to-Logrite redirect loop survives every install path.

  1. Auto-sync on install/bind (REMOVED 2026-06-05) — originally, GithubAppInstallService.completeInstall and bindInstallation called GithubAppTokenService.listInstallationRepositories(installationId) and upserted a (code_repository, project) row for every returned repo, keyed on (workspaceId, projectFullName), in a best-effort try/catch.

  2. Merged picker — GET /api/v1/agents-flow/picker/{workspaceId} returns one unified list combining DB projects, App-installed repos (lazy-create on click), and OAuth-accessible personal repos (click → install-App-on-this-repo CTA). The wizard’s MonitoringStep swaps its old DB-only fetch (/api/v1/project/{ws}/{email}) for this endpoint, threading installationId from step 2 plus the user’s gitToken / userName from Redux. OAuth-only entries open an inline modal that POSTs /install/start-for-repo to mint a pre-scoped install URL, opens it in a new tab, then offers a “refresh picker” button so the row flips from oauth → app after install. The OAuth source is skipped on the backend when oauthToken is blank, so unauthenticated sessions degrade to db + app only.

  3. Setup-URL auto-bind handler — GitHub drops state tokens on re-install and edit-repo-selection (see [GitHub App install redirect quirk]), routing the user to the App’s Setup URL (app.logrite.ai/onboarding/complete?installation_id=X&setup_action=update) with NO state. Previously the frontend had no route for this path and the user was stranded. New OnboardingComplete route at /onboarding/complete reads installation_id from window.location.search, auto-POST /install/binds with the current Redux workspace, strips the URL params (so a refresh doesn’t re-bind), and navigates to /agents-flow/onboarding with the installationId threaded via React Router location.state. If no workspace is selected (logged-out session interleaved with install), an inline banner sends the user back to step 2’s manual “Bind existing installation” link.

/sync-repos is the explicit, opt-in path to bulk-import every installed repo (e.g. after adding new repos to an existing App install). Since 2026-06-05 it is the only path that imports all repos — completeInstall / bindInstallation no longer auto-sync on bind. The wizard’s “I’ve installed it — refresh picker” button on the OAuth-only CTA re-fetches the merged picker so later-added repos appear in the dropdown without re-binding or a bulk import.

Generator vs Compliance page routing by template logType (2026-06-05)

Section titled “Generator vs Compliance page routing by template logType (2026-06-05)”

The Generator and Compliance pages share one component (Compliance.jsx renders <Generator mode="compliance">) and both fetch the same workspace project list (GET /api/v1/project/{ws}/{email}). Previously each page rendered every project in the workspace (the list was filtered by search text only), so a project showed on both pages regardless of which templates it had.

Generator.jsx now derives a mode-visible list (visibleProjects) from each project’s templates[].logType:

  • a project appears on Generator if it has ≥1 template with logType.id === 1 (general logging) — an untyped template row also counts as general;
  • a project appears on Compliance if it has ≥1 template with logType.id >= 2 (SOC 2 / HIPAA / GDPR / M-26-14);
  • a project configured for both appears on both pages (each page matches independently);
  • a project with no templates appears on neither — this hides unconfigured / previously auto-imported repos from both pages with no DB change.

The template→logType association is created when the user enables monitoring: MonitoringService.enable ensures a scan profile (a scan_profile policy; before 2026-10-15 a project_analyzer_rules_config row) of the chosen framework for each selected template (ScanProfileService.ensure), which is what the project-list response surfaces in templates[] (templateId = the policy id). See Rules & Policies.

Branch population on import + post-pick Redux refresh (post-v1i)

Section titled “Branch population on import + post-pick Redux refresh (post-v1i)”

The wizard’s step-3 branch dropdown reads from state.project.projects[].repository.branches, which is sourced from the code_branch table via GET /api/v1/project/{ws}/{email}. Before this change, every agents-flow import path (post-install/bind auto-sync, ensureProjectForRepo, manual app-source picker click) wrote code_repository + project rows but never wrote code_branch rows — only the legacy /new-project flow (UserService.getRepos) populated branches. Result: repos imported through the wizard surfaced in the picker but the branch dropdown rendered “No Items”.

Two parts:

  1. Backend — best-effort branch fetch in upsertProjectForRepo (GithubAppInstallService.populateBranchesBestEffort). On a fresh import, open a GitHubBuilder().withOAuthToken(token) client and persist every branch via CodeBranchRepository.saveAll. Token source is per-call: the installation token for source="app" / auto-sync paths, the user’s OAuth token for source="oauth". Skipped when the code_repository already has branches (legacy imports + repeat syncs) so it never double-writes. Any failure (403, 404, rate limit, network) is logged WARN and swallowed — the project import still succeeds and the picker entry still renders; the only consequence is an empty branch dropdown that the user can fall back to a text input on.

  2. Frontend — Redux refresh after ensure-project (AgentsFlowOnboarding.jsx). Both the auto-promote effect and the manual picker-click handler now re-fetch /api/v1/project/{ws}/{email} and dispatch(setProjects(...)) after a new project row is created, so the freshly-imported project (with its just-populated branches) lands in state.project.projects before the branch dropdown re-renders. The refresh is gated on entry.source !== "db" in the manual path to avoid unnecessary calls when picking a project that was already in Redux.

Both changes are additive — legacy /new-project imports were untouched, and existing rows with branches are left alone by the “skip if getBranches() non-empty” guard.

In-app add-repo + live branches endpoint (2026-05-19)

Section titled “In-app add-repo + live branches endpoint (2026-05-19)”

Two follow-ups to the merged-picker work that fix three real-world failure modes users hit after Installed-repos auto-sync + merged picker + redirect-bug fix:

  1. GitHub doesn’t reliably redirect back after an edit-installation save. GitHub’s documented Setup URL contract only fires consistently for fresh installs. Editing an existing installation (e.g. ticking a new repo in the App’s settings) ends with the user stranded on the org settings page in GitHub — no redirect to https://app.logrite.ai/onboarding/complete.
  2. “I’ve installed it — refresh picker” can’t update what GitHub doesn’t propagate. If the user picks a personal repo against an org-scoped installation, the repo will never appear in GET /installation/repositories for that installation_id, so refreshing the picker just shows the same oauth entry forever.
  3. Branch dropdown depended on Redux’s project cache. OAuth-source picks that hadn’t gone through legacy /new-project import had no state.project.projects[i].repository.branches to read from, so the dropdown rendered “No Items” until the user clicked through the wizard a second time.

Three additive changes ship together:

Adds a repo to an existing App installation server-side, using the user’s OAuth token to call PUT /user/installations/{id}/repositories/{repoId}. Skips GitHub’s install-picker UI entirely — no redirect dependency.

Request body:

{
"installationId": 132075896,
"workspaceId": 1959,
"repoFullName": "Rahil-Shah-3105/my-personal-repo",
"oauthToken": "gho_...",
"oauthUserName": "Rahil-Shah-3105"
}

Response (AddRepoToInstallationResponse):

{
"installationId": 132075896,
"workspaceId": 1959,
"repoFullName": "Rahil-Shah-3105/my-personal-repo",
"repositoryId": 84675309,
"projectId": 4912,
"defaultBranch": "main",
"added": true
}

On success the backend also runs upsertProjectForRepo + populateBranchesBestEffort in the same request, so the wizard can navigate to monitoring without a follow-up ensure-project call. The 304 idempotency case (added=false) is treated as success — the repo was already on the installation.

installationId is optional in the request body — when omitted the backend auto-derives it from OrgWorkspaceMap (most-recent installation bound to the workspace), mirroring mergedPickerList’s behaviour. The wizard relies on this when the user lands directly on step 3 (the common case for already-onboarded users): the React installationId state is only seeded by step 2’s onDone or by /onboarding/complete’s location.state, so it’s null on direct-to-step-3 entries. Without the auto-derive the user saw a misleading “No bound GitHub App installation for this workspace yet” even when the App was clearly installed and bound. If no binding exists at all, the endpoint returns 400 with “No GitHub App installation is bound to workspace X. Install the Logrite Monitoring App on your org first (agents-flow wizard step 2), then retry.” A [github-app-install] add-repo auto-derived installationId=… from workspace=… log line fires on the derive path so ops can see which binding the server picked.

GitHub-side errors propagate verbatim through HTTP 409 with IllegalStateException messages, so the user sees a real explanation:

  • Authoritative installation resolution (2026-05-19, replacing the earlier static cross-account pre-flight) — addRepoToInstallation now asks GitHub for the App’s current installations via GET /app/installations (App JWT auth, not the user’s OAuth token). The user’s gho_* token here is a classic-OAuth-App token, which GitHub rejects from /user/installations with a 403 (“must authenticate with an access token authorized to a GitHub App”). The App-JWT path doesn’t have that restriction and returns the live install IDs for every account the App is on — which is exactly what we need to bypass stale OrgWorkspaceMap rows.

    The resolver filters by account.login == ghRepo.getOwnerName() and picks an installation ID in this priority order:

    1. Caller-supplied installationId — only if it appears in the matched set.
    2. First match from the matched set, when the caller-supplied ID is missing or stale.
    3. Fallback: legacy OrgWorkspaceMap derivation (only when /app/installations is unreachable / App misconfigured — transient outages don’t break add-repo).

    This fixes the dominant cause of “HTTP 404 Installation X not visible to your account” failures: a workspace whose OrgWorkspaceMap row still points at an old installation ID (e.g. after the App was uninstalled and re-installed on the same org, which mints a new ID). A “Cross-account” error is only raised when GitHub’s /app/installations returns no install on the repo’s owner account — at that point the message names the actual owner so the user immediately understands what’s wrong.

  • Self-heal on stale OrgWorkspaceMap rows — when the GitHub-derived install ID differs from the row stored for the same orgLogin + workspaceId, the row is updated in-place. Future picker, branches, monitoring-enable calls then hit the fresh ID without needing another re-install round-trip.

  • 401 → “GitHub rejected your OAuth token (it may be expired or revoked). Sign out, log back in with GitHub, and try again.”

  • 403 (after pre-flight has ruled out cross-account) → “You don’t have admin access to <repo> or the GitHub App installation. Adding a repo to an installation requires admin permission on both…”

  • 404 → “Installation X not visible to your account, or the App’s per-repo install is restricted by the org.”

  • 422 → “This installation is set to ‘All repositories’ on GitHub — you can’t pick individual repos. Switch to ‘Only select repositories’ and retry.”

The wizard renders Cross-account responses with a warning Notice (not error) plus a “Suggested next step” pointer to the “Open GitHub install instead” secondary button — the secondary button mints a /install/start-for-repo URL pre-scoped to the OAuth user’s own account, which IS a path GitHub allows.

GET /api/v1/agents-flow/branches/{workspaceId}

Section titled “GET /api/v1/agents-flow/branches/{workspaceId}”

Returns branches for a single repo live from GitHub. Query params:

?repoFullName=Deployd-io/JobSearch
&installationId=132075896 # optional — installation token tried first
&oauthToken=gho_… # optional — used when no install token available
&oauthUserName=Rahil-Shah-3105 # optional — passed through for logging only

Response shape mirrors CodeBranchDTO[] from the legacy /api/v1/project/{ws}/{email} endpoint so the wizard can drop it straight into the same branchOptions derivation:

[
{ "branchName": "main", "commitHash": "abc123…", "shortCommitHash": "abc123" },
{ "branchName": "dev", "commitHash": "def456…", "shortCommitHash": "def456" }
]

Token preference order: installation token (cheapest rate-limit) → user OAuth → cached code_branch rows. The DB fallback covers GitHub outages and ensures the dropdown never goes empty for previously-imported repos.

  • OAuth-only modal: primary CTA is now “Add this repo to the App” which calls /install/add-repo. On success the picker entry is locally promoted from oauth → db (badge updates instantly), projectId is set from the response, and a refreshTick bump re-syncs against the server. No GitHub redirect involved. The old “Continue to GitHub” button is kept as a secondary fallback for the rare cases the in-app PUT can’t help (installation set to “All repositories”, user lacks admin on the repo) — it still mints a /install/start-for-repo URL and opens it in a new tab.

  • CTA tear-down on dropdown switch: picking any non-OAuth entry (source=db or source=app) clears oauthCtaRepo, oauthCtaUrl, and oauthCtaError in the dropdown’s onChange. Without this, an error Notice rendered against a previous OAuth pick (e.g. the 403 “no admin access” or 409 cross-account messages) stayed visible after switching to an unrelated repo, because the CTA card’s visibility is gated only on oauthCtaRepo. Switching between two OAuth entries already cleared via the OAuth branch; the gap was only the OAuth → non-OAuth transition.

  • Picker row styling — bold repo name + status pill: repo names render in font-semibold and the status suffix is a soft pill rather than inline " · App installed" / " · OAuth — install App to monitor" text. source="app" → emerald Connected pill (dot + label). source="oauth" → amber Install App pill (warning icon + label). source="db" → no badge. The picker’s options now carries an optional labelNode (JSX) alongside the plain string label; the string is still used for the input box value and search-query match, so filtering keeps working over the repo path only. The shared common/SearchableSelect was extended to render item.labelNode when present and fall back to the string label otherwise, keeping every other dropdown call site (compliance, semantic-search, etc.) unchanged.

  • Live branch fetch: every change to selectedRepoFullName fires a /agents-flow/branches/{ws} request and populates a local liveBranches state. branchOptions prefers this over the Redux project cache, so app-source and oauth-source picks (which Redux hasn’t cached yet) get a populated dropdown without depending on /api/v1/project/{ws}/{email}. The Redux cache stays as the secondary source for already-imported projects. Both db-source entries that ship branches inline (added to MergedPickerEntry in this change) and the live endpoint are one-round-trip — no N+1 explosion on the picker call itself.

  • MergedPickerEntry.branches is populated only for source="db" (from the existing code_branch cache, no extra API calls). app and oauth entries leave branches=null so the picker stays fast — the wizard fetches their branches lazily on selection via /branches.

Everything is additive. The legacy /new-project flow, existing picker behaviour for db-source picks, and the auto-promote / refresh / bind paths from the 2026-05-18 work are untouched. The 2026-05-19 changes only short-circuit the OAuth-only CTA and the branch dropdown — both of which previously required either a GitHub redirect or a Redux warm-up that wasn’t reliably present.

/userProfile perf shrink — 17s → <1s (2026-05-19)

Section titled “/userProfile perf shrink — 17s → <1s (2026-05-19)”

GET /api/v1/user/userProfile was the wizard’s slowest cold-load dependency: ~17 s wall time, 562 KB payload for a user with 24 projects × 138 cached branches (3,323 total code_branch rows). The agents-flow “Select Workspace” page sits on it before the wizard even mounts, so the 17s hang showed up as an infinite retry loop in the UI ([[project-select-workspace-retry-loop-fix]] handled the retry-loop side of that incident; this section is the server-side cure).

Two N+1 chains compounded:

(Superseded 2026-10-15: ProjectAnalyzerRulesConfigRepo no longer exists — project templates became scan-profile policies, backend c6d5c2de. The record below describes the code as it was.)

  1. projectAnalyzerRulesConfigRepo.findByProjectId per-project — one round-trip per project just to read its templates.
  2. code_repository.branches lazy fetch per-project — Hibernate ran one query per project to load the code_branch collection, then Util.toRepositoryDTO serialised every row × four fields each. The frontend’s setProjectRepos consumer reads only repoName + repositoryId (verified in useAnalyzerLogic.js) — every branch field was dead payload.

Fix (commit d05f50d):

  • Util.buildProjectResponseLight — new variant of buildProjectResponse that passes includeBranches=false through to toRepositoryDTO. The branches lazy fetch never fires; branches=[] in the response. Heavy buildProjectResponse is unchanged so the Generator / Analyzer / config endpoints that actually need branches keep their existing behaviour.

  • Batch rules-config — new ProjectAnalyzerRulesConfigRepo.findByProjectIdIn pulls every project’s rules in one query. UserService.getUserInfo groups by projectId in-memory before iterating, so each per-project render reads a Map.get instead of hitting the DB.

Local SQLite benchmark on the same data (sqlite-test Spring profile, five runs each, steady state):

MetricBaselineOptimizedΔ
Latency~200 ms~75 ms2.7× faster
Response bytes454 KB41 KB11× smaller
Cold start566 ms321 ms1.8× faster

Prod RDS impact is larger because round-trip cost dominates over JVM serialisation — 23 fewer round-trips × ~50 ms each compounds with the payload reduction. Pre-deploy live measurement was 17.5 s; verify with:

Terminal window
curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s, %{size_download} bytes\n" \
"https://leapfrogapi.deployd.io/api/v1/user/userProfile?emailId=Rahil-Shah-3105"

Downstream consumers that DO read branches (Generator, Analyzer, ProjectDetailsHeader) still get them through /api/v1/project/{ws}/{email} and /api/v1/agents-flow/branches/{ws}, neither of which changed.

/userProfile perf — round 2: per-project to-one JOIN FETCH (2026-05-21)

Section titled “/userProfile perf — round 2: per-project to-one JOIN FETCH (2026-05-21)”

After the 2026-05-19 fix shipped (d05f50d) the payload dropped to ~150 KB on prod RDS, but wall-clock stayed at ~13 s for user 479 (learnerjay, 37 projects). The original benchmark was only run on local SQLite where every query is sub-millisecond — so a per-project lazy load the benchmark hid stayed live on RDS.

The remaining N+1 fired inside the per-project loop in Util.buildProjectResponseLight:

  • project.getRepository() — @ManyToOne(LAZY), 1 select per project.
  • project.getWorkspace() — @ManyToOne default EAGER, 1 select per project.
  • repo.getAppUser() — @ManyToOne(optional=false), 1 select per repo.
  • rulesConfig.getLogType() — @ManyToOne default EAGER, 1 select per rules-config row.

For 37 projects × 3 to-ones + ~40 rules-config rows × 1 logType, RDS at ~50 ms RTT spent ~7 s in network alone — invisible on SQLite.

Fix (commit 4fd6970):

  • ProjectRepo.findByIdInWithRepoAndWorkspace — new JPQL with LEFT JOIN FETCH p.repository, LEFT JOIN FETCH p.workspace, and LEFT JOIN FETCH r.appUser. Loads the full to-one chain in one query. All joined associations are to-one, so no MultipleBagFetchException.
  • UserService.getUserInfo reloads the projects list through the new method after appUser.getProjects() resolves the ids — the lazy collection still fires once (unavoidable: AppUser.workspaces is @ManyToMany(EAGER), already a bag, so we can’t JOIN FETCH u.projects at the user level).
  • ProjectAnalyzerRulesConfigRepo.findByProjectIdIn rewritten with LEFT JOIN FETCH c.logType so the per-rules logType eager load runs once instead of per-row.

Local SQLite benchmark (warm, learnerjay, 37 projects): 120–170 ms. Prod RDS verification follows the Coolify Redeploy that auto-fires on push to main.

Normal-flow registerScan before navigation (2026-05-18)

Section titled “Normal-flow registerScan before navigation (2026-05-18)”

useScanState.isStaleStartAnalyzer keys on scanProgress.isActive to tell a genuine Begin-Scan click apart from a post-reload stale flag — but only AgentsFlowOnboarding was calling registerScan() before navigation. Normal-flow paths (handleBeginScan, handleRetryScan, handleReInitiateScan) navigated without it, so isActive was false at mount and isAnalyzing initialised to false, rendering AnalyzerSuccessScreen immediately with empty results. All three normal-flow navigations now scanProgress.registerScan(...) before navigate(...) so the mounted page correctly treats the click as genuine.

Terminal scan-progress error discrimination

Section titled “Terminal scan-progress error discrimination”

LogGeneratorService emits a final END … WebSocket message via AnalyzerMessageDTO. The DTO now carries an optional errorCode so the UI (ScanProgressContext) can distinguish failure modes without string-sniffing for null tails:

errorCodeCauseUI behavior
null (success)prLink setRender PR link, no error
auth_expiredUser’s GitHub OAuth token missing/blankperformLogout — true session expiry
pr_creation_failedToken present but GitHub rejected PR creationInline error, user stays signed in
github_app_unavailableReserved for push-mode App-install missInline error, user stays signed in
build_failedbuildPass=false with valid tokenInline error
no_changesInstrumentation produced no committable source change (or only build artifacts, which are never committed) — GitUtil.pushCode returned falseInline error, no PR opened

The pre-existing literal "END … null" wire format is preserved for legacy clients; the UI keeps a narrow fallback that only logs the user out on a bare "END null" (no extra body). Build Failed: null no longer force-logs users out — that was the original symptom of a missing org-level GitHub App install masquerading as user session expiry.

On logout, clearProjects also clears state.project.generatorPrUrl so a prior session’s persisted PR URL can’t leak into the post-re-login flow.

The Build Summary message emitted from LogGeneratorService (milestone Testing Changes) now carries three optional fields on AnalyzerMessageDTO so the UI can render typed per-file status instead of parsing the text:

FieldMeaning
buildToolWhat BuildUtil.detectBuildTool() picked (maven, gradle, npm, yarn, pip, go, …). Surfaced so misdetection is visible when the build fails for a non-file reason.
failedFilesList of source files the per-language parser flagged. Empty when the build returned non-zero but no per-file errors were extractable.
buildOutputTailLast ~500 chars of captured build stdout when the build failed without per-file errors. Gives users enough context (dependency resolution, plugin failure, etc.) without needing Coolify host access.

Three emit branches (replacing the old binary pass/fail):

  1. buildPass=true → “Build Passed: X/Y files compiled successfully” (messageType=success, empty failedFiles).
  2. buildPass=false && failedCount > 0 → “Build Summary: X passed, Y failed out of Z files” + one failed Build: <file> per entry (messageType=error, failedFiles populated).
  3. buildPass=false && failedCount == 0 → “Build failed (no per-file errors detected) — buildTool=… Check the build output tail below.” (messageType=error, buildOutputTail populated). Replaces the misleading legacy 0 failed out of N files line.

Checkout paths are allocated per-scan at /tmp/logrite-orch/{projectId}-{epochMs} on the Coolify backend host (ScanOrchestratorService.java:190) and cleaned up after the run — they do not live on a per-user scans/{username}/ directory.

Problem (2026-05-28): the generator opened a PR on a target repo containing only node_modules/ and no source — Deployd-io/banking_node_mongo PR #1 had 845,798 additions, 100% node_modules. Root cause chain:

  1. The target repo had no .gitignore.
  2. The post-injection build (BuildUtil.buildProject, npm) ran npm install && npm run build, so npm install populated node_modules/, and npm run build failed with “Missing script: build” (backend Express/Node apps have no build script).
  3. GitUtil.pushCode staged with a blind git add ., sweeping the unignored node_modules/ into the commit, and push/PR ran regardless of the build result.

Fixes:

  • GitUtil.pushCode is now hardened (returns boolean):
    • Writes a defensive set of patterns (node_modules/, dist/, build/, out/, target/, .gradle/, .next/, .nuxt/, coverage/, vendor/, __pycache__/, .venv/, venv/, bin/, obj/, …) into the checkout’s .git/info/exclude before staging. info/exclude is local-only (never committed) and JGit’s AddCommand honours it just like .gitignore — so artifacts stay out even when the repo ships no .gitignore.
    • Artifact tripwire: if any staged path still falls under a build-artifact dir, it throws IllegalStateException rather than commit — a failed run beats a node_modules PR.
    • Empty-commit guard: if nothing committable is staged, it returns false instead of creating an empty commit.
  • All three callers (LogGeneratorService, GatekeeperService webhook flow, CodeFixService) gate PR creation on that return value — a false means no PR is opened; LogGeneratorService emits the no_changes terminal error (see errorCode table) and does not persist a generator result, so the scan can be retried.
  • BuildUtil.buildProject npm command is now npm install && npm run build --if-present — --if-present exits 0 when the repo defines no build script, so backend Node repos no longer fail spuriously with “Missing script: build”. Frontends (CRA/Vite) with a real build script build normally.
  1. Backend deploys on push to main (dev, via the deploy watcher); prod moves when promoted.
  2. logrite-webhook deploys like any service — push to main, or redeploy it from the console at https://management.logrite.ai.
  3. (Optional cosmetic) pr_events_enabled in webhook.json — note the webhook container rewrites /config/webhook.json from its env at every start, so set LOGRITE_PR_EVENTS_ENABLED on the webhook service instead.
  4. To start observing PRs: set LOGRITE_PR_EVENTS_ENABLED=true in both the backend and the webhook compose services’ environment. The backend stays in DISABLED state until both are flipped.
  5. Real activation of comment/push modes requires v1h/v1i + the 5 GITHUB_APP_* env vars from v1c populated by an org admin.
LayerFile
Webhook event branchlogrite-webhook/webhook-server.cjs (search for Agents-flow v1g)
Backend controllerLogrite-AI/.../controller/MonitoringPrEventController.java
Backend serviceLogrite-AI/.../service/MonitoringPrEventService.java
Token exchangeLogrite-AI/.../service/GithubAppTokenService.java
Schemamonitoring_contracts (v1d), org_workspace_map (v1c) — no new tables in v1g
WizardLogrite-UI/.../components/Onboarding/AgentsFlowOnboarding.jsx
Setup-URL auto-bind handlerLogrite-UI/.../components/Onboarding/OnboardingComplete.jsx
Merged picker / ensure-project controllerLogrite-AI/.../controller/AgentsFlowController.java
Installed-repos bulk import (on-demand /sync-repos only)Logrite-AI/.../service/GithubAppInstallService.java (syncInstallationRepos, mergedPickerList, ensureProjectForRepo, startInstallForRepo); completeInstall / bindInstallation no longer call syncInstallationRepos (2026-06-05)
Generator vs Compliance page routing by template logTypeLogrite-UI/.../components/Generator/Generator.jsx (visibleProjects mode filter); Logrite-UI/.../components/Compliance/Compliance.jsx (renders <Generator mode="compliance">)
Installed-repos GitHub callLogrite-AI/.../service/GithubAppTokenService.java (listInstallationRepositories)