IMPORTANT
Every PR MUST have exactly one of these labels, the merge is blocked otherwise:
major/minor/patchpublishes a new release on merge with the corresponding semver bump.skip-releasemerges without publishing a release (use for docs, CI changes, refactors, etc.).
📝 Description
Adds the ai-handoff risk level action. After classification, a second LLM turn picks one action from the level's allowed_actions, and records it with a reason. Stacked on #2053 (ENG-680).
📣 User-facing impact
A risk level can let an AI agent choose allow, warn, block or hold for approval, limited to the actions the admin allows.
🔗 Related Issue
ENG-661
🚀 Type of Change
- 🐛 Bug fix (non-breaking change which fixes an issue)
- ✨ New feature (non-breaking change which adds functionality)
- 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- 📚 Documentation update
- 🎨 Style/UI update
- ♻️ Code refactor
- ⚡ Performance improvement
- ✅ Test update
- 🔧 Build configuration change
- 🧹 Chore
📋 Changes Made
sidecar:allowed_actions(list or object form) on a risk level, the hand-off turn (analyzer/handoff.go) with one tool per allowed action, the fixed suffix and the fail-safe to the most restrictive allowed action. Capabilityanalyzer_ai_handoff, the warnings from the ticket table, and README docs.gateway:handoff: truefilings are authorized against the level'sallowed_actions.require_approval, and reviews storevia_handoff(migration 000148). Each statement's AI decisions go to the session metadata (metadata.sidecar.ai_decisions), bounded in count, size and vocabulary.webapp_v2: "Hand off to AI agent" action, with a Hand-off card holding the prompt and the allowed actions, and the Approver card under "Hold for approval". The approvals page shows "Hand-off → Hold for approval".webapp: an "AI decisions" section on the session details page.
🧪 Testing
Result: a risk level can now let an AI agent pick allow, warn, block or hold for approval for each statement, and the audit records the pick and its reason. When the agent does not decide, the most restrictive allowed action applies. Tested on 5c7e673b against its base eng-680 1b04dfdd.
1. Does the agent pick the action, and does it fail safe?
A real sidecar in front of Postgres, with QA's stub LLM. Each # Step line says what the stub answers next. What to look at:
- Step 2: eng-680 refuses the config (
unknown field "allowed_actions"), so nothing starts. eng-661 loads both lanes. - Steps 3–6, right: allow and warn give
UPDATE 1. Block shows the agent's reason to the psql user. Hold for approval goes to the approval agent, which approves. - Steps 7–8: a refusal blocks ("the hand-off agent did not decide"). Lane
reportsallows only allow and warn: the agent callsblock, which it may not, sowarnapplies. - Last frame: Step 3's statement again is a cache hit (
LLM calls: 0) that keeps its reason. The audit hashandoff_decisionandhandoff_reasonfor each statement.
2. Does -validate warn for the ticket's table, and is an old sidecar refused?
One config per row of the warning table. eng-680 refuses all six. eng-661 loads each one and prints a note: that names the ignored setting. DURING DEPLOY: the eng-661 plane refuses to serve each config to an eng-680 sidecar build and names the capability analyzer_ai_handoff (daemon.DowngradeRiskLevels, which the plane calls at serve time).
3. Can an admin set up a hand-off in the UI?
Same seed (groups dba, sre) and the same rule name. What to look at:
- The open High risk action list: eng-661 ends with "Hand off to AI agent". eng-680 ends at "Hold for approval", so the left side can only hold for a person.
- Middle, right: the Hand-off card takes a prompt and the allowed actions (Allow unchecked). The Approver card under it sets Human,
dba, for the hold. - Last frame, overlay bottom-left (test overlay, read back after Save): eng-661 stores
action: ai-handoff, the prompt, andallowed_actionswithrequire_approval→ human,dba. The reopened form shows the same values.
What it solves: Admins can let an AI agent triage a risk level: run routine statements, warn, block, or hold the doubtful ones for a person or the approval agent, each with a recorded reason. Before, a level had one fixed action.
What QA caught on the way
QA ran 65 live Postgres scenarios and 7 DB-backed tests: all passed, no product defect. The review found three defects, all fixed:
- R1 (P1): a hand-off decision was reused for another user, another database or another quoted identifier with the same statement shape. The decision is now keyed on the exact content the agent read.
TestAHandoffDecisionIsKeyedOnWhatTheAgentRead,TestAHandoffDecisionIsNotReusedForAnotherIdentifier. - R2 (P1): the form read an empty
require_approval: {}beside a configuredrequire_reviewas the AI default, and a save dropped the human gate and its groups. The form now reads the aliases as the sidecar does, and shows two configured aliases read-only and saves them unchanged. No automated test (see Open items); the sidecar rule it mirrors is inTestAllowedActionsDecodeAndEncodeBothForms. - R3 (P2): a sidecar token could grow a session's AI decision metadata without bound. Each field now keeps a vocabulary value or a bounded reason, and the list has a byte ceiling.
TestPostEventsBoundsTheAIDecisions,TestPlanSidecarSessionAIDecisionVocabulary,TestPlanSidecarSessionCapsAIDecisionBytes. - A hand-off filing is authorized against the config served to that build: a build without
analyzer_ai_handoffgets 422.TestAnOldBuildIsRefusedAHandoffLevel.
Acceptance criteria
-
ai-handoffmakes a second LLM turn after classification, with the same provider and model (live relay,llm-requests.jsonl) - The agent receives only the tools for
allowed_actionsand cannot selectai-handoff(TestAHandoff*, live restricted lanes) -
allowed_actionsaccepts the list form and the object form (TestAllowedActionsDecodeAndEncodeBothForms) -
require_approvalfrom the agent usesallowed_actions.require_approvalfor both approver types (live human and AI holds;TestAHandoffHoldIsAuthorizedByItsAllowedActions,TestAHandoffAgentDecisionIsFiledDecided) - The fixed suffix is always appended, also after a user prompt (56 captured requests)
- A failed or invalid decision applies the most restrictive allowed action (live matrix: no call, two calls, disallowed tool, missing reason, refusal, timeout, provider error)
- The hand-off call counts against
max_callsand uses the cache (live budgets 1/2/3;TestAHandoffDecisionIsKeyedOnWhatTheAgentRead) -
-validatewarns for every row of the warning table - The UI sets
ai-handoffwith a prompt, allowed actions and an approval configuration (save and read back) - Docs:
sidecar/README.md - Session details show the hand-off decision with live data (licensed QA on
b33e3400; needsexperimental.sidecar_session_events) - UI matches DES-222: Figma has no frame for the hand-off settings; the card follows the Approver card style
Re-run
(cd sidecar && go test ./...) && (cd sidecar/analyzer/vertex && CGO_ENABLED=0 go test ./...)
(cd gateway && go test -timeout 50m ./models/ ./api/sidecar/ ./services/... ./api/session/ ./api/review/)
(cd sidecar/cmd && go run . -validate -config <file with an ai-handoff level>)
(cd webapp_v2 && npm run lint && npm run build)Test Configuration:
- Browser(s): Chromium (rec.mjs)
- OS: macOS (arm64)
Tests performed:
- Unit tests pass
- Integration tests pass (CI pending)
- Manual testing completed (sidecar + Postgres + stub LLM; UI on isolated gateways)
📸 Screenshots (if applicable)
See Testing.
✅ Checklist
- My code follows the style guidelines of this project
- I have performed a self-review of my own code
- I have commented my code, particularly in hard-to-understand areas
- My changes generate no new warnings
- New and existing unit tests pass locally with my changes
- I have checked my code and corrected any misspellings
📄 Additional Notes
Risk: medium.
- Stacked on #2053: merge that first.
- Sidecar↔plane: a hand-off level needs capability
analyzer_ai_handoff. The plane refuses to serve it to an older build (no downgrade). No gateway↔agent change. - Migration 000148 adds
reviews.via_handoff. Its down expires a live hand-off review that would clash with a direct one. - The hand-off decision is cached on the exact content the agent read, so literal variants make a new call. The classification cache stays shape-keyed.
Open items
- Exercised live with the Enterprise license on
b33e3400(licensed plane, managed sidecar, Postgres; QA reportb33e3400-qa-report.json, PASS): a human hold under a hand-off filed a review, a non-member got 404, a member approved and the retry ran; the approval shows "Hand-off → Hold for approval"; the session page shows the hand-off decision and reason. Not recorded as video here. - The session "AI decisions" section needs
experimental.sidecar_session_events. With it off, the session page shows no AI decisions. - Figma has no frame for the hand-off settings. The Hand-off card follows the Approver card style.
- R2 has no automated test: webapp_v2 has no unit-test runner, and no Go test loads an empty alias beside a configured one.
- The functional QA round ran on
3cc11b80; the recordings are on5c7e673b, after the Hand-off card took the design's card style. - The agent ran against QA's stub LLM, not a real provider. The UI ran on Vite against isolated gateways (PGlite), not the packaged webapp.
- The old-build refusal is shown through
DowngradeRiskLevels, not by an eng-680 sidecar connected to a live plane. - External docs are not published yet.
Activity
30📋 API Changelog
API Changelog unknown vs. unknown
API Changes
GET /approvals
- added the optional property
/items/via_handoffto the response with the200status
GET /approvals/{id}
- added the optional property
via_handoffto the response with the200status
PUT /approvals/{id}
- added the optional property
via_handoffto the response with the200status
GET /reviews
- added the optional property
/items/via_handoffto the response with the200status
GET /reviews/{id}
- added the optional property
via_handoffto the response with the200status
PUT /reviews/{id}
- added the optional property
via_handoffto the response with the200status
GET /sessions
- added the optional property
data/items/review/allOf[#/components/schemas/openapi.SessionReview]/via_handoffto the response with the200status
GET /sessions/{session_id}
- added the optional property
review/allOf[#/components/schemas/openapi.SessionReview]/via_handoffto the response with the200status
PUT /sessions/{session_id}/approval
- added the optional property
via_handoffto the response with the200status
PUT /sessions/{session_id}/review
- added the optional property
via_handoffto the response with the200status
GET /sidecars/reviews
- added the optional property
/items/via_handoffto the response with the200status
POST /sidecars/reviews
- added the new optional request property
handoff - added the optional property
review/allOf[#/components/schemas/openapi.Review]/via_handoffto the response with the200status - added the optional property
review/allOf[#/components/schemas/openapi.Review]/via_handoffto the response with the201status
GET /sidecars/reviews/{id}
- added the optional property
via_handoffto the response with the200status
POST /sidecars/reviews/{id}/claim
- added the optional property
review/allOf[#/components/schemas/openapi.Review]/via_handoffto the response with the200status
POST /sidecars/reviews/{id}/settle
- added the optional property
review/allOf[#/components/schemas/openapi.Review]/via_handoffto the response with the200status
/agentic_review
Code Review by Qodo
🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)
1. The AI decisions panel uses Tailwind styles ✗ Dismissed
Description
The new session analysis view assigns Tailwind utility classes such as w-full, p-3, bg-[--gray-1], and focus:outline-none to the AI decisions accordion and its controls. Rendering this panel reaches the new decision list, trigger, badges, links, and focus styling, introducing prohibited Tailwind class usage across the changed UI.
Code
webapp/src/webapp/features/ai_session_analyzer/views/session_analysis.cljs[R82-88]+ [:> (.-Root Accordion) {:type "single" + :collapsible true + :class "w-full p-3 bg-[--gray-1] rounded-md border border-[--gray-3]"} + [:> (.-Item Accordion) {:value "ai-decisions" + :className "border-none"} + [:> (.-Header Accordion) + [:> (.-Trigger Accordion) {:className "group flex w-full items-center justify-between gap-3 text-left focus:outline-none focus-visible:ring focus-visible:ring-gray-500 focus-visible:ring-opacity-75"}
Relevance
●●● StrongChanged UI directly violates the stated styling rule; similar CLJS styling cleanup was accepted.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The changed Reagent session analysis view adds multiple Tailwind utility class strings to the new AI decisions accordion and its contents, including w-full, p-3, flex, focus:outline-none, and text-gray-10.
Rule 1800957: Disallow Tailwind CSS class usage in React components (Mantine-only styling)webapp/src/webapp/features/ai_session_analyzer/views/session_analysis.cljs[82-109]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The new AI decisions view uses Tailwind utility class strings in Reagent UI code, including layout, spacing, colors, and focus styles, while the project requires Mantine-only styling. ## Fix Focus Areas - webapp/src/webapp/features/ai_session_analyzer/views/session_analysis.cljs[82-88] - webapp/src/webapp/features/ai_session_analyzer/views/session_analysis.cljs[90-109] ## Recommended Fix Replace the Tailwind utility classes with the project's approved Mantine component props, existing component styles, or scoped CSS module classes. Preserve the accordion layout, visible focus indicator, spacing, colors, and text wrapping without retaining Tailwind utility names.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. Migration tests bypass shared DB setup ✗ Dismissed
Description
TestReviewViaHandoffMigrationRoundTrip starts pglite directly and manually migrates it instead of using the shared migrated-database helper. The test still runs against a resumed database state, so its independent lifecycle can diverge from the package-wide test setup and migration behavior.
Code
gateway/models/review_via_handoff_migration_test.go[R25-27]+ inst, err := pglite.Start(ctx, t.TempDir()) + require.NoError(t, err) + t.Cleanup(func() { inst.Close(ctx) })
Relevance
●●● StrongExplicit helper rule and deterministic shared-lifecycle fix; migration tests should use the package helper.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The new migration test directly calls pglite.Start and performs manual migration setup, while the checklist requires migrated embedded database tests to use pglitetest.StartMigrated(t) and the package-level shared lifecycle.
Rule 3854809: Use pglitetest helpers for migrated embedded database testsgateway/models/review_via_handoff_migration_test.go[25-36]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `TestReviewViaHandoffMigrationRoundTrip` directly starts and closes an embedded database, bypassing the required `pglitetest` lifecycle for migrated database tests. ## Fix Focus Areas - gateway/models/review_via_handoff_migration_test.go[25-36] ## Recommended Fix Refactor the test setup to use the shared `pglitetest` package and its package-level `TestMain` lifecycle. Preserve the test's need to migrate to historical versions by using a supported helper or extending the shared test helper rather than constructing and migrating the embedded database directly in the test.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
3. Resumed tests leave a catalog view unqualified ✗ Dismissed
Description
TestReviewViaHandoffMigrationRoundTrip queries pg_indexes without a schema prefix while checking the migrated review index. The resumed embedded database does not guarantee the migration search path, so this catalog lookup relies on implicit resolution instead of explicitly targeting pg_catalog.pg_indexes.
Code
gateway/models/review_via_handoff_migration_test.go[R84-85]+ require.NoError(t, models.DB.Raw(`SELECT indexdef FROM pg_indexes + WHERE schemaname = 'private' AND indexname = 'index_reviews_sidecar_level_statement'`).Row().Scan(&indexDef))
Relevance
●●● StrongSchema qualification is a deterministic SQL correctness fix explicitly required for resumed embedded-database tests.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The added SQL query uses FROM pg_indexes without a schema prefix in a test that resumes an embedded database; the checklist requires all non-default-schema tables and views in such tests to be schema-qualified.
Rule 3854814: Schema-qualify SQL identifiers in tests using resumed embedded databasesgateway/models/review_via_handoff_migration_test.go[84-85]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The resumed-database migration test references the `pg_indexes` view without an explicit schema qualification. ## Fix Focus Areas - gateway/models/review_via_handoff_migration_test.go[84-85] ## Recommended Fix Change the query to reference `pg_catalog.pg_indexes` explicitly while retaining the existing `schemaname` and `indexname` filters.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
View medium (3)
4. New UI files keep PascalCase names ✗ Dismissed
Description
The new UI modules LevelHandoff.jsx, PromptField.jsx, and SettingsCard.jsx use PascalCase filenames instead of the required kebab-case convention. Future contributors and tooling that expect normalized lowercase file names encounter inconsistent paths across the newly added components.
Code
webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/sections/LevelHandoff.jsx[1]+import { Box, Paper, Stack, Text } from '@mantine/core'
Relevance
●●● StrongNew filenames directly violate the explicit kebab-case convention, making this a straightforward naming fix.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The diff adds three JavaScript component files whose names contain uppercase letters and do not follow kebab-case. The naming checklist requires new JavaScript files and folders to use lowercase words separated by single hyphens.
Rule 1800939: Use kebab-case only for file and folder names, not for code identifierswebapp_v2/src/pages/Features/AiSessionAnalyzer/Create/sections/LevelHandoff.jsx[1-1]webapp_v2/src/pages/Features/AiSessionAnalyzer/components/PromptField.jsx[1-1]webapp_v2/src/pages/Features/AiSessionAnalyzer/components/SettingsCard.jsx[1-1]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The newly added JavaScript component files use PascalCase filenames, violating the repository naming convention that requires kebab-case file and folder names. ## Fix Focus Areas - webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/sections/LevelHandoff.jsx[1-81] - webapp_v2/src/pages/Features/AiSessionAnalyzer/components/PromptField.jsx[1-56] - webapp_v2/src/pages/Features/AiSessionAnalyzer/components/SettingsCard.jsx[1-45] ## Recommended Fix Rename the files to `level-handoff.jsx`, `prompt-field.jsx`, and `settings-card.jsx`, then update every import path that references them.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
5. The settings card uses inline styling ✓ Resolved
Description
SettingsCard passes style={{ overflow: 'hidden' }} directly to its Mantine Paper component. Every use of this shared card renders the instance-local style, making the component violate the project's restriction on inline style props.
Code
webapp_v2/src/pages/Features/AiSessionAnalyzer/components/SettingsCard.jsx[11]+ <Paper withBorder radius="lg" style={{ overflow: 'hidden' }}>
Relevance
●●● StrongRecent webapp precedent accepted replacing inline styling with stylesheet-backed styling.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The changed SettingsCard implementation adds an inline style object to a Mantine Paper component, directly matching the violation described by the checklist rule.
Rule 1800977: Disallow inline style prop usage on React componentswebapp_v2/src/pages/Features/AiSessionAnalyzer/components/SettingsCard.jsx[11-11]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `SettingsCard` uses an inline `style` object on a Mantine component, which violates the project's rule against inline style props. ## Fix Focus Areas - webapp_v2/src/pages/Features/AiSessionAnalyzer/components/SettingsCard.jsx[11-11] ## Recommended Fix Move the overflow rule into a co-located CSS module or use an approved Mantine styling API, then apply the resulting class or supported prop to `Paper` instead of passing `style={{ overflow: 'hidden' }}`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
6. Session list responses grow by up to 256 KiB per sidecar session ✓ Resolved
Description
planSidecarSession now appends up to 200 entries, capped at 256 KiB, to metadata.sidecar.ai_decisions for every statement the analyzer classifies, allowed ones included. The session list query selects s.metadata in full and toOpenApiSession returns it for every row, so a page of up to 100 sidecar sessions can carry about 25 MiB of decision history the list never shows. Every event batch also rewrites the enlarged jsonb column.
Code
gateway/services/sidecarsessions.go[R61-72]+// maxSidecarAIDecisions caps metadata.sidecar.ai_decisions per session; +// metadata.sidecar.ai_decisions_dropped counts the rest. +const maxSidecarAIDecisions = 200 + +// maxSidecarAIDecisionReasonChars bounds a reason an entry stores. The +// sidecar caps the agents' reasons below it. +const maxSidecarAIDecisionReasonChars = 4096 + +// maxSidecarAIDecisionsBytes caps the stored text of +// metadata.sidecar.ai_decisions; an entry past it is counted in +// ai_decisions_dropped. The reasons are the only free text an entry holds. +const maxSidecarAIDecisionsBytes = 256 << 10
Evidence
The cap allows up to 256 KiB of decisions per session, and one is recorded for every classified statement. The list query projects the whole metadata column, and the list handler copies Metadata into each item. The API caps limit at 100. Only the session details page reads ai_decisions, through session-analysis/ai-decisions.
gateway/services/sidecarsessions.go[281-308]gateway/models/session.go[813-825]gateway/api/session/parser.go[30-53]gateway/api/session/parser.go[135-142]gateway/api/session/session.go[440-440]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description Sidecar sessions now keep up to 256 KiB of `metadata.sidecar.ai_decisions`. The session list query (`ListSessions`) selects `s.metadata` in full and returns it for up to 100 rows, so list responses and DB reads grow by up to about 25 MiB per page. Only the session details page uses this data. ## Fix Focus Areas - gateway/models/session.go[813-825] - gateway/services/sidecarsessions.go[61-72] ## Recommended Fix In the list query, project metadata without the decisions, for example `s.metadata #- '{sidecar,ai_decisions}' AS metadata`. Keep the full column in `GetSessionByID`, which the details page uses. You could also lower `maxSidecarAIDecisionsBytes`, or store the decisions in a separate column or table so each event batch does not rewrite the large jsonb metadata.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
7. Hand-off model call spends budget on statements later rules deny ✗ Dismissed
Description
EvaluateWith calls e.handoff(...) synchronously during evaluation, so the second model turn and its takeCall() run before any later evaluator in the chain. The approval agent was built to wait for those: requestAgentApproval defers through ec.RequestReview so its call runs only after every later evaluator allowed. On a lane where a later guardrail or OPA rule denies a statement, the hand-off still spends a max_calls or rate-limit slot and adds model latency. Once the budget runs out, the hand-off fails safe to its most restrictive allowed action, or the lane denies statements outright because it holds.
Code
sidecar/analyzer/evaluator.go[R601-606]+ if action == ActionHandoff { + // risk_action stays ai-handoff; the decision and its reason ride + // beside it. A cached decision is reused, but a hold it selects + // still runs for this statement. + return e.applyHandoff(stmt, ec, notes, level, c, e.handoff(ctx, stmt, ec, level, c)) + }
Evidence
handoff calls takeCall and agentCall inline. The approval agent defers its call through ec.RequestReview so it runs only after the later evaluators allowed. The hand-off path does not defer, so later denials still cost a call. takeCall draws on the classification's shared budget.
sidecar/analyzer/handoff.go[255-276]sidecar/analyzer/approver.go[212-232]sidecar/analyzer/approver.go[342-362]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The hand-off agent's model call runs synchronously in `EvaluateWith`, before later evaluators. It spends `max_calls` and rate-limit budget even when a later rule denies the statement. The approval agent avoids this by deferring through `ec.RequestReview`. ## Fix Focus Areas - sidecar/analyzer/evaluator.go[601-606] - sidecar/analyzer/handoff.go[229-277] - sidecar/analyzer/approver.go[217-232] ## Recommended Fix When `ec != nil`, register the hand-off through `ec.RequestReview(policy.ReviewRequest{Resolve: ...})` so the model call and the `applyHandoff` result are produced only after the later evaluators allow. Fall back to the synchronous path only when there is no EvalContext. Keep the cache lookup before the deferral so a cache hit still costs nothing. If you keep the synchronous call on purpose, add a comment explaining why and note the budget cost in the README.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
8. Prompt input has no visible label 📘 Rule violation ☑ Accessibility
Description
PromptField renders the visible Your prompt text as a Text component while the textarea only receives aria-label="Your prompt" and no associated label/id pair. Users who rely on the required visible form label association cannot connect that prompt text to the focusable textarea when the editor is opened.
Code
webapp_v2/src/pages/Features/AiSessionAnalyzer/components/PromptField.jsx[R22-45]+ <Text size="sm"> + <OptionalLabel>Your prompt</OptionalLabel> + </Text> + {!open && ( + <Text size="xs" c="dimmed"> + Uses the default prompt. + </Text> + )} + </Stack> + {open ? ( + <Button variant="transparent" size="xs" px={0} leftSection={<Undo2 size={14} />} onClick={useDefault}> + Use default + </Button> + ) : ( + <Button variant="transparent" size="xs" px={0} leftSection={<SquarePen size={14} />} onClick={() => setEditing(true)}> + Edit prompt + </Button> + )} + </Group> + {open && ( + <Textarea + aria-label="Your prompt" + placeholder={placeholder} + description={helper}
Relevance
● WeakA recent accessibility finding requiring visible label association was explicitly rejected in a comparable UI context.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The newly added prompt field displays Your prompt inside a Mantine Text component, while the interactive Textarea has no id and no visible label wrapper; it only sets aria-label. The checklist requires each visible textarea to have an associated visible label or to be nested inside one.
Rule 1800943: Associate each form input with a visible <label> elementwebapp_v2/src/pages/Features/AiSessionAnalyzer/components/PromptField.jsx[22-45]webapp_v2/src/pages/Features/AiSessionAnalyzer/components/PromptField.jsx[41-52]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The prompt textarea has only an `aria-label`; the visible `Your prompt` text is not a `<label>` associated with the control. ## Fix Focus Areas - webapp_v2/src/pages/Features/AiSessionAnalyzer/components/PromptField.jsx[22-45] ## Recommended Fix Give the textarea a stable `id` and render the visible `Your prompt` text through the component's visible label support or an explicit `<label htmlFor="...">`. Keep the label association intact for every rendered prompt field.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Context sources
180 rulesbuild-ticket✅ Cross-repo context — repo relationships
repo: hoophq/documentation (sha: 52af6604) — View relationshiprepo: hoophq/internal-runtime-control (sha: f9e2bc6f) — View relationshiprepo: hoophq/hoop-benchmarks (sha: ec40a61e) — View relationshiprepo: hoophq/hoop-skills (sha: 372cfc31) — View relationshiprepo: hoophq/voight-kampff (sha: c6c71dc5) — View relationshipAuto: 🧠 Deep: Dense cross-system feature with many independent logic and authorization paths.Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR✅ Build Completed with Success, Version=2065.0.0-g98897bd
This pull request is a draft
Merging is unavailable until it is marked ready for review.
Migration Safety Analysis
Summary
MEDIUM – Rollback is destructive and irreversible; data loss risk during down migration.
Files Analyzed
gateway/migrations/000148_review_via_handoff.up.sqlgateway/migrations/000148_review_via_handoff.down.sqlFindings
Rollback Safety
.up.sqland.down.sqlfiles present.down.sqlperforms a destructiveALTER TABLE reviews DROP COLUMN IF EXISTS via_handoff, which permanently removes the column and all its data. This cannot be recovered.via_handoffis dropped, there is no way to restore the boolean values that were stored in that column during the up migration.Destructive Operations
ALTER TABLE reviews DROP COLUMN IF EXISTS via_handoffwill delete all data in thevia_handoffcolumn irreversibly.reviewstable, expiring certain hand-off reviews and marking sessions as 'done'. This modifies live data and cannot be undone by re-running the up migration.Down Migration Quality
via_handoffcolumn withDEFAULT falseand recreates the unique index to includevia_handoff.Sandbox Deploy Risk
via_handoffcolumn and modify review/session state.via_handoffvalues will be lost (reset toDEFAULT false).Best Practices
DROP INDEX IF EXISTSandALTER TABLE ... DROP COLUMN IF EXISTS, which is good defensive practice, but does not solve the data loss problem.BEGIN; ... COMMIT;, so atomicity is guaranteed.Recommendations
Clarify rollback expectations: Decide whether this migration is intended to be truly reversible:
ALTER TABLE reviews DROP COLUMN IF EXISTS via_handoff;or not exist at all, acknowledging that this is a forward-only change in production.Consider a soft-delete approach: Instead of dropping the column, consider renaming it or marking it as deprecated if rollback support is needed in the future.
Audit the business logic in down migration: Verify that expiring reviews and updating session status should happen during a rollback scenario. If these are critical state changes, they should be handled outside of migrations.
For sandbox safety: If iterative rollback is required, either:
Automated analysis by Migration Safety Check. Review carefully -- LLM analysis may miss edge cases.