You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
discover and attempt exist only as CLI subcommands today: packages/loopover-miner/bin/loopover-miner.js:205-212 dispatches discover/attempt argv straight to runDiscover(cliArgs.slice(1)) (packages/loopover-miner/lib/discover-cli.js:146) and runAttempt(cliArgs.slice(1)) (packages/loopover-miner/lib/attempt-cli.js:160). Unlike governor and the portfolio queue, there is no HTTP route for either — apps/loopover-miner-ui's only write-capable routes today are the governor pause/resume pair (apps/loopover-miner-ui/vite-governor-api.ts, matching the CLI's governor pause/governor resume) and the portfolio-queue release/requeue pair (apps/loopover-miner-ui/vite-portfolio-queue-actions-api.ts, matching queue release/queue requeue). Both are thin bridges to existing store methods, gated by apps/loopover-miner-ui/vite-auth.ts's same-origin, HttpOnly, cookie-based /api/* auth (registered first in apps/loopover-miner-ui/vite.config.ts's plugin list, so every /api/* route inherits the gate automatically — no per-route auth wiring). This task adds the equivalent pair for discover/attempt, as the first HTTP surface for the AMS miner's own action-taking commands, and is one piece of a larger effort adding a chat rail to the miner dashboard (see the miner dashboard redesign's chat scope: a shared dispatch-layer scaffolding issue plus one action-family issue each for portfolio release/requeue, governor pause/resume, and this one — discover/attempt).
A real asymmetry exists between the two CLI entry points that this task must resolve, not just call through:
runAttempt already exposes options.onResult (packages/loopover-miner/lib/attempt-cli.js:201,261,317,384,503,653), typed as AttemptCliResult in packages/loopover-miner/lib/attempt-cli.d.ts:28-31,101, invoked with the real structured outcome at every genuine result point (dry-run, rejected, worktree-failure, infeasible, blocked, final) — proven by test/unit/miner-attempt-cli.test.ts's own "REGRESSION: options.onResult is called with the real structured result at every return point, alongside the unchanged plain exit code" test. Notably, onResult is not called at any of attempt-cli.js's three reportCliFailure sites (:163 parse-error, :172 paused, :670 unexpected-error) — those stay exit-code + console-only, by design.
runDiscover has no equivalent hook. Its only outputs are a console.log (human text or, with --json, JSON.stringify(result, null, 2) at packages/loopover-miner/lib/discover-cli.js:196 and :298) and a numeric exit code — confirmed by packages/loopover-miner/lib/discover-cli.d.ts's RunDiscoverOptions type, which has no onResult field, and by test/unit/miner-discover-cli.test.ts's own console.log-capturing test style (there is nothing else to capture today).
runAttempt already routes through the Governor chokepoint internally, before any write: packages/loopover-miner/lib/attempt-runner.js:4,203 imports and calls evaluateGovernorChokepointGatePersisted from ./governor-chokepoint-persisted.js (itself composing the full kill-switch → dry-run → rate-limit → budget → non-convergence → self-reputation-throttle → self-plagiarism ladder documented in packages/loopover-engine/src/governor/chokepoint.ts). A route that calls the real, unmodified runAttempt inherits that gate for free — exactly how vite-portfolio-queue-actions-api.ts inherits vite-auth.ts's cookie gate for free by living under /api/*. discover has no analogous gate today (it isn't in the chokepoint's own action list — open_pr/file_issue/apply_labels/post_eligibility_comment/create_branch/delete_branch/generate_tests — since it only fans out+ranks+enqueues), and this task must not add one that doesn't already exist in the CLI.
Requirements
⚠️ Read this before starting. This issue adds exactly two new files (apps/loopover-miner-ui/vite-discover-api.ts, apps/loopover-miner-ui/vite-attempt-api.ts), each exporting exactly one POST route (/api/discover, /api/attempt) in the same four-piece shape every sibling API file already uses — match<Name>Route, an injectable <Name>ApiDeps type, handle<Name>Request, and <Name>ApiPlugin. Do not merge the two into one file, do not add a GET counterpart (there is no "current discover/attempt state" to read the way governor has a pause-state), and do not reimplement any piece of runDiscover's or runAttempt's internal pipeline inline in the route handler — the route's only job is marshaling an HTTP request into a call to the real, unmodified CLI entry function and marshaling its result back into an HTTP response. Reimplementing fan-out/rank/enqueue or the worktree/coding-agent/chokepoint pipeline in the route file does not satisfy this issue, even if the behavior looks equivalent, and it would silently create the "parallel or bypass route" the epic's own safety design explicitly rules out.
New files: apps/loopover-miner-ui/vite-discover-api.ts and apps/loopover-miner-ui/vite-attempt-api.ts, mirroring apps/loopover-miner-ui/vite-governor-api.ts's exact structure: matchDiscoverRoute/matchAttemptRoute (pure, synchronous, checked before any body read), DiscoverApiDeps/AttemptApiDeps (injectable so tests never touch a real store, network, or worktree), handleDiscoverRequest/handleAttemptRequest (factored out for direct unit tests, mirroring the sibling handleGovernorRequest/handlePortfolioQueueActionsRequest pattern), and discoverApiPlugin/attemptApiPlugin (the Vite configureServer/configurePreviewServer middleware wrapper).
Routes: POST /api/discover and POST /api/attempt only — no GET routes for this pair.
Registration: import and register both new plugins in apps/loopover-miner-ui/vite.config.ts, positioned afterauthPlugin() in the plugins array (same requirement vite-governor-api.ts's own header comment documents for every sibling — an unauthenticated /api/* request must never reach these handlers).
discover-cli.js needs an onResult hook added, mirroring attempt-cli.js's exact convention — not a new convention: call options.onResult?.(result) at the two existing success points only (packages/loopover-miner/lib/discover-cli.js:185-201 for the dry-run branch, :287-302 for the full-run branch), using the result object each branch already builds. Do not call onResult at any of the four existing reportCliFailure sites (discover-cli.js:149,203,212,304) — that mirrors attempt-cli.js's own established asymmetry (onResult fires only for real structured outcomes, never for parse-error/unexpected-error branches) exactly, rather than inventing a different rule for discover.
Type update: add an optional onResult?: (result: DiscoverResult) => void field to RunDiscoverOptions in packages/loopover-miner/lib/discover-cli.d.ts, reusing the already-existing DiscoverResult type there — mirror RunAttemptOptions.onResult's doc comment in attempt-cli.d.ts:98-101 (optional chaining means both branches — hook present, hook absent — need real coverage, per this repo's "test both sides of every ??/ternary" branch-coverage rule).
Request marshaling: parseDiscoverArgs/parseAttemptArgs (discover-cli.js:43, attempt-cli.js:55) are the only entry point into runDiscover/runAttempt — both take a CLI-style args: string[], not a structured options object, for their user-facing inputs. The route handler must build that args array from the parsed POST JSON body (repository/search targets, --dry-run, --api-base-url, --token-env, --json for discover; owner/repo, issue number, --miner-login, --base, --live, --dry-run, --json for attempt) rather than trying to skip argv construction — there is no lower-level structured-input entry point to call instead.
Never accept a credential in the request body. The POST body must not include (and the handler must not read) any githubToken, token, apiKey, or similar credential field. Credentials are resolved server-side exactly as the CLI already does — GITHUB_TOKEN/env, or resolveGitHubToken's live-session fetch — matching packages/loopover-engine/src/miner/local-write-tools.ts's own documented boundary that the miner's local harness runs writes with its own local credentials, never a caller-supplied one. Legitimate non-secret passthrough fields (repo targets, search query, apiBaseUrl, the name of a token-env-var via tokenEnv, dryRun, live, base, minerLogin, issue number) are fine and already match what the CLI flags accept.
Malformed body: an unparseable or missing-required-field POST body returns 400 with a structured { error: "invalid_request_body" }-shaped body, mirroring vite-portfolio-queue-actions-api.ts's existing parseActionBody convention exactly — do not let a malformed body reach runDiscover/runAttempt at all.
Response envelope: when the CLI function reports a real structured outcome via the new onResult capture, respond 200 with that captured result (plus the raw exit code, so callers can distinguish a governed rejection/paused outcome from a clean success without re-deriving it from the result shape). When the CLI function returns a non-zero exit code without ever calling onResult (the parse-error/paused/unexpected-error branches that intentionally don't fire it, per the asymmetry above), there is no structured result to return — respond with a clear error status and a plain-text-derived message rather than crashing on an assumed-present result object; this is a real branch every route test must exercise, not an edge case to skip.
/api/attempt may run for minutes, not milliseconds — it drives a full worktree checkout + coding-agent iteration, unlike every existing /api/* route in this app (all synchronous local-store reads/writes). Do not add an artificial per-request timeout at the route layer; tests must simulate a slow-resolving runAttempt via an injected fake, not real waiting.
Client fetchers: add apps/loopover-miner-ui/src/lib/discover.ts and apps/loopover-miner-ui/src/lib/attempt.ts, mirroring apps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts's/apps/loopover-miner-ui/src/lib/governor.ts's response-parsing + typed-result conventions (a { ok: true, ... } | { ok: false, error } discriminated result, never a thrown exception for an HTTP-level failure).
Chat dispatch wiring: once the shared dispatch-layer scaffolding (built by the chat action-dispatch scaffolding issue this task depends on for its final wiring step only) exists, register these two new endpoints in its action registry so a chat-triggered discover/attempt request calls POST /api/discover/POST /api/attempt and renders the structured result (or the plain-text error) inline in the message list. Reuse the single action-dispatch config flag that scaffolding issue defines — do not invent a second, per-route flag, and do not add any flag check inside vite-discover-api.ts/vite-attempt-api.ts themselves; every existing sibling route file is unconditionally registered, so the gating belongs entirely in the dispatch layer, exactly like the portfolio-queue and governor route files already work today. If that scaffolding issue has not yet merged when this issue is picked up, the two HTTP routes, the onResult hook, and the client fetchers (everything above this bullet) are still fully buildable and testable on their own and should ship regardless — only the final dispatch-registration step is blocked on it.
Out of scope: collapsing any routed dashboard page into a rail-card summary; new grounding/read data (calibration, ranked-candidates, track-record); anything under sidebar.tsx as primary navigation; a bypass-chokepoint execution path; Spec: conversational chat interface for loopover (Lovable/Cursor-style) #6230's separate maintainer-chat scope.
apps/loopover-miner-ui/vite.config.ts updated to import + register both new plugins after authPlugin()
packages/loopover-miner/lib/discover-cli.js gains an options.onResult?.(result) call at its two existing success points (:185-201, :287-302), with RunDiscoverOptions.onResult added to packages/loopover-miner/lib/discover-cli.d.ts
apps/loopover-miner-ui/src/lib/discover.ts and apps/loopover-miner-ui/src/lib/attempt.ts — typed client fetchers
Test files covering the new route handlers, the onResult addition, and the client fetchers (see Test Coverage Requirements)
Chat dispatch-layer registration for both new endpoints, gated behind the shared action-dispatch config flag (once the scaffolding issue's registry exists)
Test Coverage Requirements
packages/loopover-miner/lib/discover-cli.js is Codecov-gated (coverage: in codecov.yml collects packages/loopover-miner/lib/**) at the standing 99% patch bar — every line and branch the onResult addition touches (including both sides of the options.onResult?.(...) optional-chaining call — hook provided and hook absent — at both call sites) needs real coverage in test/unit/miner-discover-cli.test.ts, following the exact pattern test/unit/miner-attempt-cli.test.ts's own "REGRESSION: options.onResult is called with the real structured result at every return point..." test already establishes for runAttempt.
apps/loopover-miner-ui/** is explicitly excluded from Codecov (codecov.yml's ignore: - "apps/**") — patch coverage does not gate the two new route files or the client fetchers. The real local gate instead is apps/loopover-miner-ui/vitest.config.ts's own coverage thresholds (currently statements: 85, branches: 85, functions: 75, lines: 85 — a floor, not a ratchet, per that file's own comment), enforced via npm run ui:test (part of npm run test:ci's ui:test step). The new files must be covered well enough not to drag the whole-package percentages below that floor, matching the discipline already applied to every sibling vite-*-api.ts file.
Add apps/loopover-miner-ui/src/discover-api.test.ts and apps/loopover-miner-ui/src/attempt-api.test.ts (plain API-test files with no UI component to combine with yet, mirroring apps/loopover-miner-ui/src/run-state-api.test.ts's shape rather than governor.test.tsx's combined shape), importing handleDiscoverRequest/handleAttemptRequest directly from ../vite-discover-api/../vite-attempt-api exactly as run-state-api.test.ts imports handleRunStateRequest from ../vite-run-state-api. Required cases per route:
match<Name>Route returns the route for the correct method+path and null for every other method/path combination (including the sibling routes' own paths, to prove no cross-matching).
A well-formed POST body reaches the injected fake runDiscover/runAttempt and its captured onResult payload is what the route returns.
A malformed/missing-required-field POST body returns 400without ever calling the injected runDiscover/runAttempt fake.
The exit-code-only branch (fake resolves non-zero without ever calling the injected onResult) is handled without throwing and produces a structured error response — this is the branch most likely to be missed, since every existing sibling route always has a structured result to return.
/api/attempt specifically: a slow-resolving fake (a Promise the test resolves manually, not a real timer/sleep) proves the handler doesn't impose its own timeout.
Neither route's injected fake ever receives a githubToken/token/apiKey-shaped field from a POST body that included one — the field is dropped, not threaded through.
Add a regression test in test/unit/miner-discover-cli.test.ts proving runDiscover's plain exit-code return is unchanged by the onResult addition (mirroring attempt-cli.d.ts:28-30's own stated backward-compatibility contract: onResult is additive, never a replacement for the exit code bin/loopover-miner.js's process.exit(exitCode) usage still relies on).
Expected Outcome
POST /api/discover and POST /api/attempt exist as real, authenticated, tested HTTP routes in apps/loopover-miner-ui, each a thin, non-bypassing bridge to the real runDiscover/runAttempt CLI entry points — inheriting vite-auth.ts's cookie gate and (for attempt) the Governor chokepoint automatically, with no new parallel execution path. runDiscover gains a structured onResult hook matching runAttempt's own proven convention, closing the asymmetry that made discover's result invisible to anything but a human reading stdout. Once the chat dispatch-layer scaffolding lands, a chat-triggered "run discover on acme/widgets" or "attempt acme/widgets#42" call renders its real structured result inline in the message list, through the exact same write path the CLI and the future chat surface both share.
Links & Resources
apps/loopover-miner-ui/vite-governor-api.ts — the pattern to mirror (route matcher / injectable deps / handler / plugin shape)
apps/loopover-miner-ui/vite-portfolio-queue-actions-api.ts — the pattern to mirror for a POST-only action pair + malformed-body handling
apps/loopover-miner-ui/vite-auth.ts — the /api/* auth gate every route (including the two new ones) inherits automatically
apps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts, apps/loopover-miner-ui/src/lib/governor.ts — client-fetcher patterns to mirror
apps/loopover-miner-ui/src/run-state-api.test.ts, apps/loopover-miner-ui/src/governor.test.tsx, apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx — existing route-handler test conventions
packages/loopover-miner/bin/loopover-miner.js:205-212 — current CLI-only dispatch for discover/attempt
packages/loopover-miner/lib/discover-cli.js (:146runDiscover, :185-201/:287-302 success points, :149,203,212,304reportCliFailure sites) and discover-cli.d.ts
packages/loopover-miner/lib/attempt-cli.js (:160runAttempt, :201,261,317,384,503,653onResult sites) and attempt-cli.d.ts (:28-31,98-101AttemptCliResult/onResult typing)
test/unit/miner-discover-cli.test.ts, test/unit/miner-attempt-cli.test.ts — existing CLI-level test suites to extend
packages/loopover-miner/lib/attempt-runner.js:4,203, packages/loopover-miner/lib/governor-chokepoint-persisted.js, packages/loopover-engine/src/governor/chokepoint.ts — the chokepoint gate runAttempt already routes through and this task must not bypass
packages/loopover-engine/src/miner/local-write-tools.ts — the "miner's own local credentials, never a caller-supplied one" boundary behind the no-credential-in-body requirement
codecov.yml, apps/loopover-miner-ui/vitest.config.ts — the two different coverage gates in play (Codecov patch for packages/loopover-miner/lib/**, the local vitest floor for apps/loopover-miner-ui)
The miner dashboard redesign's chat action-dispatch scaffolding issue, and its portfolio release/requeue and governor pause/resume sibling issues — this task's siblings in the same epic
Context
discoverandattemptexist only as CLI subcommands today:packages/loopover-miner/bin/loopover-miner.js:205-212dispatchesdiscover/attemptargv straight torunDiscover(cliArgs.slice(1))(packages/loopover-miner/lib/discover-cli.js:146) andrunAttempt(cliArgs.slice(1))(packages/loopover-miner/lib/attempt-cli.js:160). Unlike governor and the portfolio queue, there is no HTTP route for either —apps/loopover-miner-ui's only write-capable routes today are the governor pause/resume pair (apps/loopover-miner-ui/vite-governor-api.ts, matching the CLI'sgovernor pause/governor resume) and the portfolio-queue release/requeue pair (apps/loopover-miner-ui/vite-portfolio-queue-actions-api.ts, matchingqueue release/queue requeue). Both are thin bridges to existing store methods, gated byapps/loopover-miner-ui/vite-auth.ts's same-origin,HttpOnly, cookie-based/api/*auth (registered first inapps/loopover-miner-ui/vite.config.ts's plugin list, so every/api/*route inherits the gate automatically — no per-route auth wiring). This task adds the equivalent pair fordiscover/attempt, as the first HTTP surface for the AMS miner's own action-taking commands, and is one piece of a larger effort adding a chat rail to the miner dashboard (see the miner dashboard redesign's chat scope: a shared dispatch-layer scaffolding issue plus one action-family issue each for portfolio release/requeue, governor pause/resume, and this one — discover/attempt).A real asymmetry exists between the two CLI entry points that this task must resolve, not just call through:
runAttemptalready exposesoptions.onResult(packages/loopover-miner/lib/attempt-cli.js:201,261,317,384,503,653), typed asAttemptCliResultinpackages/loopover-miner/lib/attempt-cli.d.ts:28-31,101, invoked with the real structured outcome at every genuine result point (dry-run, rejected, worktree-failure, infeasible, blocked, final) — proven bytest/unit/miner-attempt-cli.test.ts's own"REGRESSION: options.onResult is called with the real structured result at every return point, alongside the unchanged plain exit code"test. Notably,onResultis not called at any ofattempt-cli.js's threereportCliFailuresites (:163parse-error,:172paused,:670unexpected-error) — those stay exit-code + console-only, by design.runDiscoverhas no equivalent hook. Its only outputs are aconsole.log(human text or, with--json,JSON.stringify(result, null, 2)atpackages/loopover-miner/lib/discover-cli.js:196and:298) and a numeric exit code — confirmed bypackages/loopover-miner/lib/discover-cli.d.ts'sRunDiscoverOptionstype, which has noonResultfield, and bytest/unit/miner-discover-cli.test.ts's own console.log-capturing test style (there is nothing else to capture today).runAttemptalready routes through the Governor chokepoint internally, before any write:packages/loopover-miner/lib/attempt-runner.js:4,203imports and callsevaluateGovernorChokepointGatePersistedfrom./governor-chokepoint-persisted.js(itself composing the full kill-switch → dry-run → rate-limit → budget → non-convergence → self-reputation-throttle → self-plagiarism ladder documented inpackages/loopover-engine/src/governor/chokepoint.ts). A route that calls the real, unmodifiedrunAttemptinherits that gate for free — exactly howvite-portfolio-queue-actions-api.tsinheritsvite-auth.ts's cookie gate for free by living under/api/*.discoverhas no analogous gate today (it isn't in the chokepoint's own action list —open_pr/file_issue/apply_labels/post_eligibility_comment/create_branch/delete_branch/generate_tests— since it only fans out+ranks+enqueues), and this task must not add one that doesn't already exist in the CLI.Requirements
apps/loopover-miner-ui/vite-discover-api.tsandapps/loopover-miner-ui/vite-attempt-api.ts, mirroringapps/loopover-miner-ui/vite-governor-api.ts's exact structure:matchDiscoverRoute/matchAttemptRoute(pure, synchronous, checked before any body read),DiscoverApiDeps/AttemptApiDeps(injectable so tests never touch a real store, network, or worktree),handleDiscoverRequest/handleAttemptRequest(factored out for direct unit tests, mirroring the siblinghandleGovernorRequest/handlePortfolioQueueActionsRequestpattern), anddiscoverApiPlugin/attemptApiPlugin(the ViteconfigureServer/configurePreviewServermiddleware wrapper).POST /api/discoverandPOST /api/attemptonly — no GET routes for this pair.apps/loopover-miner-ui/vite.config.ts, positioned afterauthPlugin()in thepluginsarray (same requirementvite-governor-api.ts's own header comment documents for every sibling — an unauthenticated/api/*request must never reach these handlers).discover-cli.jsneeds anonResulthook added, mirroringattempt-cli.js's exact convention — not a new convention: calloptions.onResult?.(result)at the two existing success points only (packages/loopover-miner/lib/discover-cli.js:185-201for the dry-run branch,:287-302for the full-run branch), using theresultobject each branch already builds. Do not callonResultat any of the four existingreportCliFailuresites (discover-cli.js:149,203,212,304) — that mirrorsattempt-cli.js's own established asymmetry (onResultfires only for real structured outcomes, never for parse-error/unexpected-error branches) exactly, rather than inventing a different rule for discover.onResult?: (result: DiscoverResult) => voidfield toRunDiscoverOptionsinpackages/loopover-miner/lib/discover-cli.d.ts, reusing the already-existingDiscoverResulttype there — mirrorRunAttemptOptions.onResult's doc comment inattempt-cli.d.ts:98-101(optional chaining means both branches — hook present, hook absent — need real coverage, per this repo's "test both sides of every??/ternary" branch-coverage rule).parseDiscoverArgs/parseAttemptArgs(discover-cli.js:43,attempt-cli.js:55) are the only entry point intorunDiscover/runAttempt— both take a CLI-styleargs: string[], not a structured options object, for their user-facing inputs. The route handler must build thatargsarray from the parsed POST JSON body (repository/search targets,--dry-run,--api-base-url,--token-env,--jsonfor discover;owner/repo, issue number,--miner-login,--base,--live,--dry-run,--jsonfor attempt) rather than trying to skip argv construction — there is no lower-level structured-input entry point to call instead.githubToken,token,apiKey, or similar credential field. Credentials are resolved server-side exactly as the CLI already does —GITHUB_TOKEN/env, orresolveGitHubToken's live-session fetch — matchingpackages/loopover-engine/src/miner/local-write-tools.ts's own documented boundary that the miner's local harness runs writes with its own local credentials, never a caller-supplied one. Legitimate non-secret passthrough fields (repo targets, search query,apiBaseUrl, the name of a token-env-var viatokenEnv,dryRun,live,base,minerLogin, issue number) are fine and already match what the CLI flags accept.400with a structured{ error: "invalid_request_body" }-shaped body, mirroringvite-portfolio-queue-actions-api.ts's existingparseActionBodyconvention exactly — do not let a malformed body reachrunDiscover/runAttemptat all.onResultcapture, respond200with that captured result (plus the raw exit code, so callers can distinguish a governed rejection/paused outcome from a clean success without re-deriving it from the result shape). When the CLI function returns a non-zero exit code without ever callingonResult(the parse-error/paused/unexpected-error branches that intentionally don't fire it, per the asymmetry above), there is no structured result to return — respond with a clear error status and a plain-text-derived message rather than crashing on an assumed-present result object; this is a real branch every route test must exercise, not an edge case to skip./api/attemptmay run for minutes, not milliseconds — it drives a full worktree checkout + coding-agent iteration, unlike every existing/api/*route in this app (all synchronous local-store reads/writes). Do not add an artificial per-request timeout at the route layer; tests must simulate a slow-resolvingrunAttemptvia an injected fake, not real waiting.apps/loopover-miner-ui/src/lib/discover.tsandapps/loopover-miner-ui/src/lib/attempt.ts, mirroringapps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts's/apps/loopover-miner-ui/src/lib/governor.ts's response-parsing + typed-result conventions (a{ ok: true, ... } | { ok: false, error }discriminated result, never a thrown exception for an HTTP-level failure).POST /api/discover/POST /api/attemptand renders the structured result (or the plain-text error) inline in the message list. Reuse the single action-dispatch config flag that scaffolding issue defines — do not invent a second, per-route flag, and do not add any flag check insidevite-discover-api.ts/vite-attempt-api.tsthemselves; every existing sibling route file is unconditionally registered, so the gating belongs entirely in the dispatch layer, exactly like the portfolio-queue and governor route files already work today. If that scaffolding issue has not yet merged when this issue is picked up, the two HTTP routes, theonResulthook, and the client fetchers (everything above this bullet) are still fully buildable and testable on their own and should ship regardless — only the final dispatch-registration step is blocked on it.sidebar.tsxas primary navigation; a bypass-chokepoint execution path; Spec: conversational chat interface for loopover (Lovable/Cursor-style) #6230's separate maintainer-chat scope.Deliverables
apps/loopover-miner-ui/vite-discover-api.ts—matchDiscoverRoute,DiscoverApiDeps,handleDiscoverRequest,discoverApiPlugin, mirroringvite-governor-api.tsapps/loopover-miner-ui/vite-attempt-api.ts—matchAttemptRoute,AttemptApiDeps,handleAttemptRequest,attemptApiPlugin, mirroringvite-governor-api.tsapps/loopover-miner-ui/vite.config.tsupdated to import + register both new plugins afterauthPlugin()packages/loopover-miner/lib/discover-cli.jsgains anoptions.onResult?.(result)call at its two existing success points (:185-201,:287-302), withRunDiscoverOptions.onResultadded topackages/loopover-miner/lib/discover-cli.d.tsapps/loopover-miner-ui/src/lib/discover.tsandapps/loopover-miner-ui/src/lib/attempt.ts— typed client fetchersonResultaddition, and the client fetchers (see Test Coverage Requirements)Test Coverage Requirements
packages/loopover-miner/lib/discover-cli.jsis Codecov-gated (coverage:incodecov.ymlcollectspackages/loopover-miner/lib/**) at the standing 99% patch bar — every line and branch theonResultaddition touches (including both sides of theoptions.onResult?.(...)optional-chaining call — hook provided and hook absent — at both call sites) needs real coverage intest/unit/miner-discover-cli.test.ts, following the exact patterntest/unit/miner-attempt-cli.test.ts's own"REGRESSION: options.onResult is called with the real structured result at every return point..."test already establishes forrunAttempt.apps/loopover-miner-ui/**is explicitly excluded from Codecov (codecov.yml'signore: - "apps/**") — patch coverage does not gate the two new route files or the client fetchers. The real local gate instead isapps/loopover-miner-ui/vitest.config.ts's own coveragethresholds(currentlystatements: 85, branches: 85, functions: 75, lines: 85— a floor, not a ratchet, per that file's own comment), enforced vianpm run ui:test(part ofnpm run test:ci'sui:teststep). The new files must be covered well enough not to drag the whole-package percentages below that floor, matching the discipline already applied to every siblingvite-*-api.tsfile.apps/loopover-miner-ui/src/discover-api.test.tsandapps/loopover-miner-ui/src/attempt-api.test.ts(plain API-test files with no UI component to combine with yet, mirroringapps/loopover-miner-ui/src/run-state-api.test.ts's shape rather thangovernor.test.tsx's combined shape), importinghandleDiscoverRequest/handleAttemptRequestdirectly from../vite-discover-api/../vite-attempt-apiexactly asrun-state-api.test.tsimportshandleRunStateRequestfrom../vite-run-state-api. Required cases per route:match<Name>Routereturns the route for the correct method+path andnullfor every other method/path combination (including the sibling routes' own paths, to prove no cross-matching).runDiscover/runAttemptand its capturedonResultpayload is what the route returns.400without ever calling the injectedrunDiscover/runAttemptfake.onResult) is handled without throwing and produces a structured error response — this is the branch most likely to be missed, since every existing sibling route always has a structured result to return./api/attemptspecifically: a slow-resolving fake (aPromisethe test resolves manually, not a real timer/sleep) proves the handler doesn't impose its own timeout.githubToken/token/apiKey-shaped field from a POST body that included one — the field is dropped, not threaded through.test/unit/miner-discover-cli.test.tsprovingrunDiscover's plain exit-code return is unchanged by theonResultaddition (mirroringattempt-cli.d.ts:28-30's own stated backward-compatibility contract:onResultis additive, never a replacement for the exit codebin/loopover-miner.js'sprocess.exit(exitCode)usage still relies on).Expected Outcome
POST /api/discoverandPOST /api/attemptexist as real, authenticated, tested HTTP routes inapps/loopover-miner-ui, each a thin, non-bypassing bridge to the realrunDiscover/runAttemptCLI entry points — inheritingvite-auth.ts's cookie gate and (for attempt) the Governor chokepoint automatically, with no new parallel execution path.runDiscovergains a structuredonResulthook matchingrunAttempt's own proven convention, closing the asymmetry that made discover's result invisible to anything but a human reading stdout. Once the chat dispatch-layer scaffolding lands, a chat-triggered "run discover on acme/widgets" or "attempt acme/widgets#42" call renders its real structured result inline in the message list, through the exact same write path the CLI and the future chat surface both share.Links & Resources
apps/loopover-miner-ui/vite-governor-api.ts— the pattern to mirror (route matcher / injectable deps / handler / plugin shape)apps/loopover-miner-ui/vite-portfolio-queue-actions-api.ts— the pattern to mirror for a POST-only action pair + malformed-body handlingapps/loopover-miner-ui/vite-auth.ts— the/api/*auth gate every route (including the two new ones) inherits automaticallyapps/loopover-miner-ui/vite.config.ts— plugin registration + orderingapps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts,apps/loopover-miner-ui/src/lib/governor.ts— client-fetcher patterns to mirrorapps/loopover-miner-ui/src/run-state-api.test.ts,apps/loopover-miner-ui/src/governor.test.tsx,apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx— existing route-handler test conventionspackages/loopover-miner/bin/loopover-miner.js:205-212— current CLI-only dispatch fordiscover/attemptpackages/loopover-miner/lib/discover-cli.js(:146runDiscover,:185-201/:287-302success points,:149,203,212,304reportCliFailuresites) anddiscover-cli.d.tspackages/loopover-miner/lib/attempt-cli.js(:160runAttempt,:201,261,317,384,503,653onResultsites) andattempt-cli.d.ts(:28-31,98-101AttemptCliResult/onResulttyping)test/unit/miner-discover-cli.test.ts,test/unit/miner-attempt-cli.test.ts— existing CLI-level test suites to extendpackages/loopover-miner/lib/attempt-runner.js:4,203,packages/loopover-miner/lib/governor-chokepoint-persisted.js,packages/loopover-engine/src/governor/chokepoint.ts— the chokepoint gaterunAttemptalready routes through and this task must not bypasspackages/loopover-engine/src/miner/local-write-tools.ts— the "miner's own local credentials, never a caller-supplied one" boundary behind the no-credential-in-body requirementcodecov.yml,apps/loopover-miner-ui/vitest.config.ts— the two different coverage gates in play (Codecov patch forpackages/loopover-miner/lib/**, the local vitest floor forapps/loopover-miner-ui)@loopover/ui-kit) and Spec: conversational chat interface for loopover (Lovable/Cursor-style) #6230 (the separate, explicitly out-of-scope maintainer-chat spec) — related epic context