Agents-flow
What Agents-flow is
Section titled “What Agents-flow is”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.
Slice status (as of 2026-05-12)
Section titled “Slice status (as of 2026-05-12)”| Slice | What it ships | Status |
|---|---|---|
| v1a–v1a.4 | 10 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 |
| v1b | create_workspace chat tool (first WRITE — wraps existing POST /api/v1/auth/workspace) | ✅ shipped |
| v1c | start_github_app_install chat tool + POST /api/v1/github/app/install/start + GET /api/v1/github/app/install/callback + org_workspace_map table | ✅ shipped |
| v1d | enable_monitoring chat tool + POST /api/v1/monitoring/enable + monitoring_contracts table + stub provisioner | ✅ shipped |
| v1e | verify_monitoring, initiate_scan, get_scan_report chat tools + GET /api/v1/monitoring/{id}/health + POST /api/v1/scan/initiate (stub orchestrator behind flag) | ✅ shipped |
| v1f | get_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 |
| v1f | 4-step React onboarding wizard at /agents-flow/onboarding (consumes the same backend endpoints as the chat tools) | ✅ shipped |
| v1g | GitHub pull_request event intake — logrite-webhook extension + POST /api/v1/monitoring/pr-event + GithubAppTokenService + stub mode-routing switch (log-only) | ✅ shipped |
| v1h | Comment-mode goes live — real GitUtil.postPrComment behind LOGRITE_PR_COMMENT_ENABLED | ✅ shipped |
| v1i | Push-mode goes live — real fix-branch + sibling-PR behind LOGRITE_PR_PUSH_ENABLED | ✅ shipped |
Endpoints
Section titled “Endpoints”| Method | Path | Shipped in | Description |
|---|---|---|---|
POST | /api/v1/github/app/install/start | v1c | Begin GitHub App install flow — returns install URL (or 503 when env vars blank) |
GET | /api/v1/github/app/install/callback | v1c | OAuth-style callback from GitHub — binds installation_id to workspace_id |
POST | /api/v1/monitoring/enable | v1d | Enable 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}/health | v1e | Read monitoring contract state + computed issues |
POST | /api/v1/scan/initiate | v1e | Initiate 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-event | v1g | Receive a GitHub pull_request event forwarded by logrite-webhook |
POST | /api/v1/github/app/install/sync-repos | post-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-repo | post-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-project | post-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. |
POST /api/v1/monitoring/pr-event (v1g)
Section titled “POST /api/v1/monitoring/pr-event (v1g)”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:
| Status | When |
|---|---|
DISABLED | LOGRITE_PR_EVENTS_ENABLED=false (default — safe-ship) |
NO_WORKSPACE | No org_workspace_map row matches installationId (see installation_id ↔ workspace cardinality) |
NO_PROJECT | Workspace has no project matching repoFullName |
NO_CONTRACT | Project exists but enable_monitoring never ran |
STUB_LOGGED | Full chain ran — last_webhook_at stamped, scan orchestrator called, mode arm logged (v1g log-only) |
FAILED | Exception — detail in message |
installation_id ↔ workspace cardinality
Section titled “installation_id ↔ workspace cardinality”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:
| Caller | Lookup now uses | Semantics |
|---|---|---|
GET /api/v1/github/app/install/installations (wizard picker) | findFirstByInstallationIdOrderByInstalledAtDescIdDesc | newest-wins — bound and boundToWorkspaceId reflect the most-recently-installed workspace |
POST /api/v1/github/app/install/bind | findByInstallationIdAndWorkspaceId | exact (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-event | findByInstallationIdAndInstalledAtIsNotNullOrderByInstalledAtDescIdDesc | fan-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:
| Slice | mode behaviour |
|---|---|
| post-v1d (today’s prod) | Persisted in monitoring_contracts. No consumer. |
| post-v1f | Same — wizard collects the value but it remains dead data. |
| post-v1g | First 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-v1h | comment 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-v1i | push 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. |
Feature flags
Section titled “Feature flags”| Env var | Default | Effect when true |
|---|---|---|
LOGRITE_PROVISIONER_ENABLED | false | v1d provisioner creates real OpenSearch index / dashboards / monitors (currently throws provisioner_real_not_implemented until OS-side templates land) |
LOGRITE_SCAN_ORCHESTRATOR_ENABLED | false | v1e /scan/initiate runs the real clone-at-ref → analyzer chain (verified through stage 4/6 locally) |
LOGRITE_PR_EVENTS_ENABLED | false | v1g — /monitoring/pr-event actually processes; webhook forwards events. With this off, both sides 200 silently. |
LOGRITE_PR_COMMENT_ENABLED | false | v1h — flip on to enable real PR comments via GitUtil.postPrComment (per-project gate is monitoring_contracts.mode='comment') |
LOGRITE_PR_PUSH_ENABLED | false | v1i — flip on to enable real fix-branch + sibling-PR via GatekeeperService.generateLogs (per-project gate is monitoring_contracts.mode='push') |
v1g GithubAppTokenService
Section titled “v1g GithubAppTokenService”Exchanges the App’s private key for a per-installation access token:
- 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)
- POST
https://api.github.com/app/installations/{id}/access_tokenswith the JWT as bearer - 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.
Webhook-side (logrite-webhook v1g)
Section titled “Webhook-side (logrite-webhook v1g)”The webhook server now branches on event type:
push→ existing auto-deploy path (Logrite-UI / logrite-analyzer / Logrite-AI onmainanddev)pull_request(action inopened|synchronize|reopened) → forward to backendPOST /api/v1/monitoring/pr-event- other actions → 200 silently
Config (env var wins over webhook.json):
| Source | Field / Var | Default |
|---|---|---|
| env | LOGRITE_PR_EVENTS_ENABLED | unset |
| webhook.json | pr_events_enabled | false |
| env | LOGRITE_BACKEND_URL | unset |
| webhook.json | backend_url | http://localhost:8080 |
Forwarding is async + 5s timeout; failures log but don’t retry (GitHub re-delivers on the next PR sync).
Wizard at /agents-flow/onboarding
Section titled “Wizard at /agents-flow/onboarding”It was a 3-step React component mirroring the chat tool chain:
-
Pick or create workspace (defaults to Redux
currentWorkSpace) -
Install the GitHub App (button calls
/github/app/install/startand opens the GitHub install URL — shows yellow notice when503 not configured) -
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), withstate.project.projects[].repository.branchesas a secondary fallback for legacy db-source picks. The legacy fallback comes fromGET /api/v1/project/{ws}/{email}— note that the/api/v1/user/userProfilepayload 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’smaindefault; 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/initiateonly. 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/:scanIdwithstate: { startAnalyzer: true, autoApproveAndGenerate: true }. The scan-results page auto-chains analyzer → approve-all → submit → generator and never surfaces theAnalyzerSuccessScreen. 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
autoApproveAndGenerateflag is captured at mount inuseScanState(location.state is wiped on mount byuseScanResultsLogic) 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 byautoApprovedRefinuseScanStateto prevent double-fire.Orchestrator-already-ran short-circuit gate.
useAnalyzerLogicprobesGET /api/v1/scan-results/{id}and skips opening the analyzer WebSocket +POST /api/v1/analyzerwhen the persisted scan row already carries findings (combo flow: orchestrator ran AnalyzerService server- side before the UI mounted). This gate is keyed strictly onlocationState.autoApproveAndGenerate— the normal “Begin Scan” path from Generator only setsstartAnalyzer:trueand lands on/generator/project/:id/scanwhereparams.idis the project id; gating onstartAnalyzeralone and probing byparams.idlookup 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 toparams.scanId || scanIdso a project id is never sent as a scan-results path param. Fix shipped 2026-05-18. - Run scan (secondary) — fires
Sidebar entry “Agents-flow” added in commit 77abc6ad.
Analyzer WS DTO unwrap (2026-05-18)
Section titled “Analyzer WS DTO unwrap (2026-05-18)”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.
-
Auto-sync on install/bind (REMOVED 2026-06-05) — originally,
GithubAppInstallService.completeInstallandbindInstallationcalledGithubAppTokenService.listInstallationRepositories(installationId)and upserted a(code_repository, project)row for every returned repo, keyed on(workspaceId, projectFullName), in a best-effort try/catch. -
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’sMonitoringStepswaps its old DB-only fetch (/api/v1/project/{ws}/{email}) for this endpoint, threadinginstallationIdfrom step 2 plus the user’sgitToken/userNamefrom Redux. OAuth-only entries open an inline modal that POSTs/install/start-for-repoto mint a pre-scoped install URL, opens it in a new tab, then offers a “refresh picker” button so the row flips fromoauth→appafter install. The OAuth source is skipped on the backend whenoauthTokenis blank, so unauthenticated sessions degrade todb + apponly. -
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. NewOnboardingCompleteroute at/onboarding/completereadsinstallation_idfromwindow.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/onboardingwith the installationId threaded via React Routerlocation.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:
-
Backend — best-effort branch fetch in
upsertProjectForRepo(GithubAppInstallService.populateBranchesBestEffort). On a fresh import, open aGitHubBuilder().withOAuthToken(token)client and persist every branch viaCodeBranchRepository.saveAll. Token source is per-call: the installation token forsource="app"/ auto-sync paths, the user’s OAuth token forsource="oauth". Skipped when thecode_repositoryalready has branches (legacy imports + repeat syncs) so it never double-writes. Any failure (403, 404, rate limit, network) is loggedWARNand 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. -
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}anddispatch(setProjects(...))after a new project row is created, so the freshly-imported project (with its just-populated branches) lands instate.project.projectsbefore the branch dropdown re-renders. The refresh is gated onentry.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:
- 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. - “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/repositoriesfor that installation_id, so refreshing the picker just shows the sameoauthentry forever. - Branch dropdown depended on Redux’s project cache. OAuth-source picks
that hadn’t gone through legacy
/new-projectimport had nostate.project.projects[i].repository.branchesto read from, so the dropdown rendered “No Items” until the user clicked through the wizard a second time.
Three additive changes ship together:
POST /api/v1/github/app/install/add-repo
Section titled “POST /api/v1/github/app/install/add-repo”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) —
addRepoToInstallationnow asks GitHub for the App’s current installations viaGET /app/installations(App JWT auth, not the user’s OAuth token). The user’sgho_*token here is a classic-OAuth-App token, which GitHub rejects from/user/installationswith 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 staleOrgWorkspaceMaprows.The resolver filters by
account.login == ghRepo.getOwnerName()and picks an installation ID in this priority order:- Caller-supplied
installationId— only if it appears in the matched set. - First match from the matched set, when the caller-supplied ID is missing or stale.
- Fallback: legacy
OrgWorkspaceMapderivation (only when/app/installationsis 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
OrgWorkspaceMaprow 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/installationsreturns 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. - Caller-supplied
-
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 onlyResponse 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.
Frontend wizard changes
Section titled “Frontend wizard changes”-
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 fromoauth→db(badge updates instantly),projectIdis set from the response, and arefreshTickbump 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-repoURL and opens it in a new tab. -
CTA tear-down on dropdown switch: picking any non-OAuth entry (
source=dborsource=app) clearsoauthCtaRepo,oauthCtaUrl, andoauthCtaErrorin the dropdown’sonChange. 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 onoauthCtaRepo. 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-semiboldand 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’soptionsnow carries an optionallabelNode(JSX) alongside the plain stringlabel; the string is still used for the input box value and search-query match, so filtering keeps working over the repo path only. The sharedcommon/SearchableSelectwas extended to renderitem.labelNodewhen 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
selectedRepoFullNamefires a/agents-flow/branches/{ws}request and populates a localliveBranchesstate.branchOptionsprefers this over the Redux project cache, soapp-source andoauth-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. Bothdb-source entries that shipbranchesinline (added toMergedPickerEntryin this change) and the live endpoint are one-round-trip — no N+1 explosion on the picker call itself. -
MergedPickerEntry.branchesis populated only forsource="db"(from the existingcode_branchcache, no extra API calls).appandoauthentries leavebranches=nullso 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.)
projectAnalyzerRulesConfigRepo.findByProjectIdper-project — one round-trip per project just to read its templates.code_repository.brancheslazy fetch per-project — Hibernate ran one query per project to load thecode_branchcollection, thenUtil.toRepositoryDTOserialised every row × four fields each. The frontend’ssetProjectReposconsumer reads onlyrepoName+repositoryId(verified inuseAnalyzerLogic.js) — every branch field was dead payload.
Fix (commit d05f50d):
-
Util.buildProjectResponseLight— new variant ofbuildProjectResponsethat passesincludeBranches=falsethrough totoRepositoryDTO. The branches lazy fetch never fires;branches=[]in the response. HeavybuildProjectResponseis unchanged so the Generator / Analyzer / config endpoints that actually need branches keep their existing behaviour. -
Batch rules-config — new
ProjectAnalyzerRulesConfigRepo.findByProjectIdInpulls every project’s rules in one query.UserService.getUserInfogroups byprojectIdin-memory before iterating, so each per-project render reads aMap.getinstead of hitting the DB.
Local SQLite benchmark on the same data (sqlite-test Spring profile,
five runs each, steady state):
| Metric | Baseline | Optimized | Δ |
|---|---|---|---|
| Latency | ~200 ms | ~75 ms | 2.7× faster |
| Response bytes | 454 KB | 41 KB | 11× smaller |
| Cold start | 566 ms | 321 ms | 1.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:
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()—@ManyToOnedefault EAGER, 1 select per project.repo.getAppUser()—@ManyToOne(optional=false), 1 select per repo.rulesConfig.getLogType()—@ManyToOnedefault 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 withLEFT JOIN FETCH p.repository,LEFT JOIN FETCH p.workspace, andLEFT JOIN FETCH r.appUser. Loads the full to-one chain in one query. All joined associations are to-one, so no MultipleBagFetchException.UserService.getUserInforeloads the projects list through the new method afterappUser.getProjects()resolves the ids — the lazy collection still fires once (unavoidable:AppUser.workspacesis@ManyToMany(EAGER), already a bag, so we can’tJOIN FETCH u.projectsat the user level).ProjectAnalyzerRulesConfigRepo.findByProjectIdInrewritten withLEFT JOIN FETCH c.logTypeso 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:
errorCode | Cause | UI behavior |
|---|---|---|
null (success) | prLink set | Render PR link, no error |
auth_expired | User’s GitHub OAuth token missing/blank | performLogout — true session expiry |
pr_creation_failed | Token present but GitHub rejected PR creation | Inline error, user stays signed in |
github_app_unavailable | Reserved for push-mode App-install miss | Inline error, user stays signed in |
build_failed | buildPass=false with valid token | Inline error |
no_changes | Instrumentation produced no committable source change (or only build artifacts, which are never committed) — GitUtil.pushCode returned false | Inline 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.
Build-step structured payload
Section titled “Build-step structured payload”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:
| Field | Meaning |
|---|---|
buildTool | What BuildUtil.detectBuildTool() picked (maven, gradle, npm, yarn, pip, go, …). Surfaced so misdetection is visible when the build fails for a non-file reason. |
failedFiles | List of source files the per-language parser flagged. Empty when the build returned non-zero but no per-file errors were extractable. |
buildOutputTail | Last ~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):
buildPass=true→ “Build Passed: X/Y files compiled successfully” (messageType=success, emptyfailedFiles).buildPass=false && failedCount > 0→ “Build Summary: X passed, Y failed out of Z files” + onefailed Build: <file>per entry (messageType=error,failedFilespopulated).buildPass=false && failedCount == 0→ “Build failed (no per-file errors detected) — buildTool=… Check the build output tail below.” (messageType=error,buildOutputTailpopulated). Replaces the misleading legacy0 failed out of N filesline.
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.
Build-artifact safeguard & empty-PR guard
Section titled “Build-artifact safeguard & empty-PR guard”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:
- The target repo had no
.gitignore. - The post-injection build (
BuildUtil.buildProject, npm) rannpm install && npm run build, sonpm installpopulatednode_modules/, andnpm run buildfailed with “Missing script: build” (backend Express/Node apps have nobuildscript). GitUtil.pushCodestaged with a blindgit add ., sweeping the unignorednode_modules/into the commit, and push/PR ran regardless of the build result.
Fixes:
GitUtil.pushCodeis now hardened (returnsboolean):- 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/excludebefore staging.info/excludeis local-only (never committed) and JGit’sAddCommandhonours 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
IllegalStateExceptionrather than commit — a failed run beats a node_modules PR. - Empty-commit guard: if nothing committable is staged, it returns
falseinstead of creating an empty commit.
- Writes a defensive set of patterns (
- All three callers (
LogGeneratorService,GatekeeperServicewebhook flow,CodeFixService) gate PR creation on that return value — afalsemeans no PR is opened;LogGeneratorServiceemits theno_changesterminal error (see errorCode table) and does not persist a generator result, so the scan can be retried. BuildUtil.buildProjectnpm command is nownpm install && npm run build --if-present—--if-presentexits 0 when the repo defines nobuildscript, so backend Node repos no longer fail spuriously with “Missing script: build”. Frontends (CRA/Vite) with a realbuildscript build normally.
Ops actions to take v1g live
Section titled “Ops actions to take v1g live”- Backend deploys on push to
main(dev, via the deploy watcher); prod moves when promoted. - logrite-webhook deploys like any service — push to
main, or redeploy it from the console athttps://management.logrite.ai. - (Optional cosmetic)
pr_events_enabledinwebhook.json— note the webhook container rewrites/config/webhook.jsonfrom its env at every start, so setLOGRITE_PR_EVENTS_ENABLEDon thewebhookservice instead. - To start observing PRs: set
LOGRITE_PR_EVENTS_ENABLED=truein both thebackendand thewebhookcompose services’ environment. The backend stays in DISABLED state until both are flipped. - Real activation of comment/push modes requires v1h/v1i + the 5
GITHUB_APP_*env vars from v1c populated by an org admin.
Where to look in the code
Section titled “Where to look in the code”| Layer | File |
|---|---|
| Webhook event branch | logrite-webhook/webhook-server.cjs (search for Agents-flow v1g) |
| Backend controller | Logrite-AI/.../controller/MonitoringPrEventController.java |
| Backend service | Logrite-AI/.../service/MonitoringPrEventService.java |
| Token exchange | Logrite-AI/.../service/GithubAppTokenService.java |
| Schema | monitoring_contracts (v1d), org_workspace_map (v1c) — no new tables in v1g |
| Wizard | Logrite-UI/.../components/Onboarding/AgentsFlowOnboarding.jsx |
| Setup-URL auto-bind handler | Logrite-UI/.../components/Onboarding/OnboardingComplete.jsx |
| Merged picker / ensure-project controller | Logrite-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 logType | Logrite-UI/.../components/Generator/Generator.jsx (visibleProjects mode filter); Logrite-UI/.../components/Compliance/Compliance.jsx (renders <Generator mode="compliance">) |
| Installed-repos GitHub call | Logrite-AI/.../service/GithubAppTokenService.java (listInstallationRepositories) |