Skip to content

[AI-2156] kcap setup creates the flow and polls it to completion - #640

Merged
George-Payne merged 5 commits into
mainfrom
georgepayne/ai-2156-setup-creates-and-polls-flow
Aug 24, 2026
Merged

[AI-2156] kcap setup creates the flow and polls it to completion#640
George-Payne merged 5 commits into
mainfrom
georgepayne/ai-2156-setup-creates-and-polls-flow

Conversation

@George-Payne

Copy link
Copy Markdown
Member

Gives the server's two rendezvous routes a caller. They shipped with none, so nothing generated a flow id and the browser's claim-on-arrival was what established ownership - which is where it sat under the retired pairing, and is the one property of the design the server half could not realise alone.

The leg runs after login, since both routes are authenticated. It generates a 128-bit base64url id, creates the flow, opens {server}/setup?s=<id>, and polls until every step it knows has settled.

  • FirstRunFlowId - 16 CSPRNG bytes as base64url, 22 characters. The server's floor is what makes that the only shape that fits; it can check length and alphabet but never entropy, so the guarantee is the generator's alone.
  • FirstRunFlowClient - the two routes, degrading rather than throwing. Refusals are handled apart: 404/401/403/405 on the create mean the tenant does not serve the flow and say nothing to the user; 429 reports the server's own Retry-After rather than sleeping through ten minutes of it; 409 retries with a fresh id, since it means the id is taken rather than the credentials wrong.
  • FirstRunFlowPoll - the poll's decision, extracted so every branch is tested without a socket. 410 is a dead link, 404 a flow that will never be ours, 401 a re-login rather than a new link, and 5xx or a transport blip is another tick.
  • FirstRunFlowOutcomes - outcomes, never instructions. Step and status strings map onto closed local sets and an unrecognised member is dropped, because kcap setup writes Claude Code hooks and a hook entry is a command string Claude Code runs. Which steps are gates stays the server's to say, through can_finish, rather than being restated here where an old CLI could get it wrong.
  • BrowserFirstRunFlow - create, then open, then poll. The setup URL is composed locally, so unlike the pairing there is no server-supplied URL reaching a shell-executed open to validate.
  • SetupCommand - an unnumbered leg after login. Skipped on --no-prompt and the None provider. Headless deliberately is not a skip: the link is printed as well as opened, which is what keeps the screens available to the device-path population rather than designing it out of them.
  • Any key ends the wait. The 30-minute budget is the backstop for a terminal nobody is sitting at; a closed tab should not cost half an hour of dots.

Two things worth flagging for review. 401/403 on the create are read as "no flow here" even though the route is authenticated - a gateway answering them on a path it does not know is indistinguishable from the feature being off, and a login succeeded seconds earlier, so guessing wrong here silently skips an additive leg while guessing the other way prints an alarming auth failure on every tenant that has the flow off. And the leg reports, configuring nothing: the screens that would push configuration are their own tickets, so the terminal steps remain what wires the machine up, and which of the two renders a given step is a decision that belongs to neither.

Unblocks two things the server half deferred: refusing a flow no CLI created (and with it metering the claim path, which is unlimited today), and s being single-consumer.

Capacitor.Cli.Core.Tests.Unit and Capacitor.Cli.Tests.Unit are green apart from WriteAndBootstrap_writes_the_unit_and_bootstraps_without_a_leading_bootout, which fails identically on an unmodified tree - it refuses a group-writable temp directory, which is a devcontainer artefact rather than anything here. AOT publish is clean of IL2026/IL3050.

AI-2156

The server's two rendezvous routes shipped with no caller at all, so nothing
generated a flow id and the browser's claim-on-arrival was what established
ownership - which is where it sat under the retired pairing, and the one
property of the design the server half could not realise alone.

The leg runs after login, since both routes are authenticated. It generates a
128-bit base64url id, creates the flow, opens {server}/setup?s=<id>, and polls
until every step it knows has settled.

- Refusals are handled apart: 404/401/403/405 on the create mean the tenant
  does not serve the flow, and say nothing; 429 reports the server's own
  Retry-After rather than sleeping through it; 409 retries with a fresh id,
  since it means the id is taken rather than the credentials wrong.
- The poll's decision is extracted and unit-tested per branch. 410 is a dead
  link, 404 a flow that will never be ours, 401 a re-login rather than a new
  link, and 5xx or a transport blip is another tick.
- Outcomes, never instructions. Step and status strings map onto closed local
  sets and an unrecognised member is dropped, because kcap setup writes Claude
  Code hooks and a hook entry is a command string Claude Code runs. Which steps
  are gates stays the server's to say, via can_finish.
- The setup URL is composed locally, so unlike the pairing there is no
  server-supplied URL reaching a shell-executed open to validate.
- Any key ends the wait. The 30-minute budget is the backstop for a terminal
  nobody is sitting at; a closed tab should not cost half an hour of dots.
- Headless is deliberately not a skip - the link is printed as well as opened,
  which is what keeps the screens available to the device-path population.

The leg reports and configures nothing: the screens that would push
configuration are their own tickets, and the terminal steps remain what wires
the machine up.
@George-Payne George-Payne self-assigned this Aug 21, 2026
@linear-code

linear-code Bot commented Aug 21, 2026

Copy link
Copy Markdown

AI-2156

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Create and poll browser first-run setup flow during kcap setup

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Create an authenticated first-run flow ID before opening /setup, then poll until finished.
• Add resilient HTTP client + poll classification, including keypress escape hatch and rate-limit
 handling.
• Document the browser-finishing leg and add thorough unit tests for all branches.
Diagram

sequenceDiagram
  actor U as "User"
  participant SC as "SetupCommand"
  participant BF as "BrowserFirstRunFlow"
  participant FC as "FirstRunFlowClient"
  participant API as "Tenant API"
  participant B as "Browser"

  U->>SC: "kcap setup"
  SC->>BF: "RunAsync(serverUrl, machine)"
  BF->>FC: "CreateAsync(flowId)"
  FC->>API: "POST /api/first-run/flows"
  API-->>FC: "200 (flow state) | 404/401/403/405 | 409 | 429"
  FC-->>BF: "FirstRunCreateOutcome"
  BF->>B: "Open server/setup?s=<flowId>"
  loop "poll until finished / timeout / keypress"
    BF->>FC: "PollAsync(flowId)"
    FC->>API: "GET /api/first-run/flows/<flowId>"
    API-->>FC: "200 (state) | 410 | 404 | 401/403 | 429 | 5xx/transport"
    FC-->>BF: "FirstRunPollOutcome"
  end
  BF-->>SC: "FirstRunFlowResult"
  SC-->>U: "one-line outcome; continue setup"
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Server-minted flow IDs (CLI requests create-without-id)
  • ➕ Removes client responsibility for entropy/shape guarantees
  • ➕ Eliminates 409 collision retry logic from the CLI
  • ➖ Undermines the key design goal (CLI owns the flow before browser arrives) unless create is still authenticated and strongly bound to caller
  • ➖ Would require server API changes and coordinated rollout
2. Push-based completion (SSE/WebSocket) instead of polling
  • ➕ Lower server load and better UX responsiveness without frequent GETs
  • ➕ More natural place for rate limiting/backpressure
  • ➖ Significantly more complexity in CLI networking and server infrastructure
  • ➖ Harder to make robust across proxies, corporate networks, and CLI environments
3. Long-polling with server-side wait
  • ➕ Reduces request frequency while keeping simple HTTP semantics
  • ➕ Can provide near-real-time completion without 2s polling cadence
  • ➖ More server complexity and resource holding
  • ➖ Still needs timeout/backoff behavior and careful cancellation handling

Recommendation: Keep the PR’s current approach (client-minted 128-bit ID, create-then-open, short-interval polling with explicit verdict classification). It best matches the ownership model goal while remaining operationally simple and robust in typical CLI networking conditions; the code already mitigates risks via degraded outcomes, bounded retries/backoff, and comprehensive unit tests.

Files changed (18) +1511 / -0

Enhancement (10) +685 / -0
BrowserFirstRunFlow.csImplement create-open-poll orchestration for first-run flow +174/-0

Implement create-open-poll orchestration for first-run flow

• Introduces the main browser-first-run controller: generate ID, create flow (with conflict retries), open locally-composed setup URL, and poll until finished/terminal/budget. Adds keypress-to-dismiss behavior, 429 backoff, and detailed status handling.

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs

FirstRunFlowClient.csAdd HTTP client seam for first-run flow create/poll routes +90/-0

Add HTTP client seam for first-run flow create/poll routes

• Adds an HttpClient-based implementation of create and poll requests that returns outcomes instead of throwing on transient errors. Handles JSON serialization/deserialization, Retry-After parsing for 429, and preserves HTTP status vs transport failure (status 0).

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs

FirstRunFlowId.csGenerate 128-bit base64url flow IDs (22 chars) +21/-0

Generate 128-bit base64url flow IDs (22 chars)

• Adds a dedicated generator for first-run flow IDs using CSPRNG 16 bytes and base64url encoding. Encodes design constraint that the server can validate length/alphabet but not entropy.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowId.cs

FirstRunFlowModels.csDefine request/response models for first-run flow API +45/-0

Define request/response models for first-run flow API

• Adds wire DTOs for POST create and flow state response, with snake_case JSON property names. Captures step, can_finish gate, and per-step outcomes mapping used by polling logic.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowModels.cs

FirstRunFlowOutcomes.csMap wire step/outcome strings onto closed local enums +108/-0

Map wire step/outcome strings onto closed local enums

• Introduces closed enums for known steps and outcomes plus mapping helpers that drop unknown values. Implements IsFinished based on server’s can_finish plus all known steps being settled, ensuring unknown new-server steps don’t stall old CLIs.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowOutcomes.cs

FirstRunFlowPoll.csExtract pure poll verdict classifier for HTTP responses +60/-0

Extract pure poll verdict classifier for HTTP responses

• Adds a deterministic classifier mapping status/body readability to loop verdicts (state/expired/gone/unauthenticated/slowdown/wait). Enables unit testing of poll semantics without network dependencies.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowPoll.cs

FirstRunFlowProgress.csAdd progress interface for rendering browser-leg UX +21/-0

Add progress interface for rendering browser-leg UX

• Defines an abstraction for rendering “opening browser”, poll ticks, and wait-ended behavior. Allows SetupCommand to supply Spectre.Console-based output while keeping core flow logic testable.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowProgress.cs

FirstRunFlowResult.csDefine result model for browser leg outcomes +38/-0

Define result model for browser leg outcomes

• Adds a discriminated result set capturing finished, expired, abandoned (budget), dismissed (keypress), unavailable (feature off), rate limited, and failed states. Ensures the browser leg never throws for reachable failures and setup can continue.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowResult.cs

Models.csRegister first-run flow DTOs for source-generated JSON +2/-0

Register first-run flow DTOs for source-generated JSON

• Adds JsonSerializable registrations for CreateFirstRunFlowRequest and FirstRunFlowResponse in the shared JSON context. Ensures the new HTTP client uses the same serialization infrastructure as the rest of the CLI.

src/Capacitor.Cli.Core/Models.cs

SetupCommand.csRun browser first-run flow after login with Spectre output +126/-0

Run browser first-run flow after login with Spectre output

• Adds an unnumbered post-login browser leg that creates and polls the first-run flow when available. Implements Spectre.Console progress rendering and a single-line outcome summary, skipping on --no-prompt and AuthProvider.None while continuing terminal setup regardless.

src/Capacitor.Cli/Commands/SetupCommand.cs

Tests (6) +804 / -0
BrowserFirstRunFlowTests.csAdd unit tests for create-open-poll loop, backoff, and keypress +422/-0

Add unit tests for create-open-poll loop, backoff, and keypress

• Introduces a fake channel, fake clock, and fake key watcher to test ordering (create before open), URL composition, 409 retry behavior, terminal statuses, 429 slowdown, polling budget, and keypress dismissal/drain semantics.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/BrowserFirstRunFlowTests.cs

FirstRunFlowClientTests.csTest wire contract for first-run flow HTTP client +158/-0

Test wire contract for first-run flow HTTP client

• Uses WireMock to verify create/poll paths, snake_case fields, Retry-After parsing, trailing slash tolerance, and handling of unreadable JSON vs transport failure. Guards against silent “unavailable” regressions caused by wrong paths or fields.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowClientTests.cs

FirstRunFlowIdTests.csTest flow ID length, alphabet, and non-repetition +31/-0

Test flow ID length, alphabet, and non-repetition

• Pins the generator to 22-character base64url output and verifies allowed characters. Adds a small uniqueness check to catch accidental determinism/regressions.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowIdTests.cs

FirstRunFlowOutcomesTests.csTest closed-set mapping and finish criteria enforcement +114/-0

Test closed-set mapping and finish criteria enforcement

• Verifies that unknown step/outcome strings are dropped (read as pending) and that can_finish gates completion. Ensures newer-server extra steps don’t stall old CLIs and that non-gate failures don’t prevent finish.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowOutcomesTests.cs

FirstRunFlowPollTests.csTest poll response classification matrix +32/-0

Test poll response classification matrix

• Covers each branch of FirstRunFlowPoll.Classify, including unreadable 200 bodies, 404-as-gone semantics, and unauthenticated vs retryable conditions. Ensures unexpected responses don’t silently spin without a correct terminal interpretation.

test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowPollTests.cs

SetupCommandTests.csTest SetupCommand’s browser-leg outcome messaging +47/-0

Test SetupCommand’s browser-leg outcome messaging

• Adds tests to ensure only Finished is treated as success, timeouts/expiry warn, dismissal does not warn, rate limits are rounded up to whole minutes, and failure messages are escaped for Spectre markup safety.

test/Capacitor.Cli.Tests.Unit/Commands/SetupCommandTests.cs

Documentation (2) +22 / -0
README.mdDocument browser-based finishing step in setup flow +15/-0

Document browser-based finishing step in setup flow

• Adds an explicit description of the post-login browser setup leg, including sample output and guidance for headless/remote completion. Clarifies that setup continues in-terminal and that waiting can be dismissed with any key.

README.md

help-setup.txtUpdate setup help text to describe browser-finishing leg +7/-0

Update setup help text to describe browser-finishing leg

• Documents that, after sign-in, setup may open a browser link and wait for completion, with printed URL fallback and keypress escape. Notes it is skipped under --no-prompt and on servers without the feature.

src/Capacitor.Cli.Core/Resources/help-setup.txt

@qodo-code-review

qodo-code-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Caps server retry delay ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
The polling backoff caps every delay at 30 seconds even when the server supplies a longer
Retry-After. A server asking for 60 seconds will be polled again after 30 seconds and repeatedly
receive requests before its advertised rate-limit window ends.
Code

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[R219-221]

+        if (next < PollInterval) next = PollInterval;
+
+        return next > MaxInterval ? MaxInterval : next;
Evidence
Backoff selects the supplied retry delay but then returns MaxInterval whenever it exceeds 30
seconds. The client parses Retry-After values without imposing that cap, and its tests demonstrate
that a poll response can carry 60 seconds, so that valid value reaches the truncating code path.

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[167-171]
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[216-221]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[76-86]
test/Capacitor.Cli.Core.Tests.Unit/FirstRun/FirstRunFlowClientTests.cs[217-230]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Polling truncates a server-provided `Retry-After` to 30 seconds. This violates the server's requested rate-limit delay and can cause repeated 429 responses.

## Issue Context
`FirstRunFlowClient` parses both delta and date-form `Retry-After` headers, and the poll path passes that value into `Backoff`. A 60-second header is explicitly covered by the client tests.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[216-221]
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[167-172]
- test/Capacitor.Cli.Core.Tests.Unit/FirstRun/BrowserFirstRunFlowTests.cs[487-494]

Preserve the maximum backoff only for locally calculated exponential delays, or otherwise allow a valid server-provided `Retry-After` to exceed that cap. Add a poll-loop test using a `Retry-After` longer than 30 seconds.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Cancellation swallowed in client ✓ Resolved 🐞 Bug ☼ Reliability
Description
FirstRunFlowClient treats OperationCanceledException as a transient transport blip and converts it
to StatusCode=0, which prevents CancellationToken cancellation (e.g., Ctrl+C / host shutdown) from
aborting browser setup polling. This can leave setup stuck polling until the 30-minute PollBudget
instead of stopping promptly on cancellation.
Code

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[R88-89]

+    static bool IsTransient(Exception e) =>
+        e is HttpRequestException or OperationCanceledException or JsonException or NotSupportedException;
Evidence
The new client explicitly includes OperationCanceledException in its transient filter, so any
cancellation (including caller-requested) is flattened into StatusCode=0. In contrast, existing
polling flows in the repo only treat OperationCanceledException as transient when the caller token
was not canceled, or they rethrow when the caller token is canceled.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[61-74]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[88-89]
src/Capacitor.Cli.Core/Auth/OAuthLoginFlow.cs[211-219]
src/Capacitor.Cli.Core/Config/ServerUrlNormalizer.cs[114-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FirstRunFlowClient` currently classifies `OperationCanceledException` as transient and degrades it to `StatusCode = 0`. This swallows legitimate caller cancellation (`ct.IsCancellationRequested == true`), which means higher-level cancellation cannot stop the browser setup flow promptly.

## Issue Context
Elsewhere in the codebase, cancellation is explicitly preserved (either rethrown or only treated as transient when the *caller token* is not canceled).

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-74]
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[76-90]

### Implementation notes
- Update `CreateAsync` / `PollAsync` / `ReadAsync` to **rethrow** `OperationCanceledException` when `ct.IsCancellationRequested` is true.
- Only degrade `OperationCanceledException` to status 0 when it represents a timeout or other non-caller cancellation (i.e., `!ct.IsCancellationRequested`).
- One simple pattern:
 - `catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return new(0, null); }`
 - `catch (HttpRequestException) { ... }` etc.
- Remove `OperationCanceledException` from the generic `IsTransient(Exception e)` helper, or replace the helper with overloads that can examine `ct`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Immediate dismissal key discarded ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
PollAsync drains buffered input only after Opening has displayed “Press any key” and returned,
so a user who responds immediately can have that valid dismissal key discarded as stale input. The
CLI then continues waiting until another key is pressed, the browser finishes, or the budget
expires.
Code

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[R118-120]

+        // A keypress that preceded this leg — the Return that confirmed an earlier step — is not an
+        // answer to "press any key to carry on here". Drained once, so only presses from here on count.
+        if (_keys.CanWatch && _keys.KeyAvailable) _keys.Drain();
Evidence
The opening callback is invoked before PollAsync, and the concrete callback prints the any-key
instruction. PollAsync then unconditionally drains any key already available, proving that input
entered after the instruction but before the drain is discarded.

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[99-120]
src/Capacitor.Cli/Commands/SetupCommand.cs[66-75]

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 browser-flow polling method drains available keyboard input after the user has already been shown the dismissal prompt. A key pressed immediately in response to that prompt can therefore be mistaken for stale input and discarded.

## Issue Context
`RunAsync` calls `progress.Opening(setupUrl)` before entering `PollAsync`, and `SpectreFirstRunFlowProgress.Opening` prints “Press any key to carry on here instead.” The stale-input drain must happen before that prompt becomes actionable, while later keypresses must continue to produce `Dismissed`.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[99-120]
- src/Capacitor.Cli/Commands/SetupCommand.cs[66-75]
- test/Capacitor.Cli.Core.Tests.Unit/FirstRun/BrowserFirstRunFlowTests.cs[418-442]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. RunBrowserFlowStepAsync comment too verbose ✓ Resolved 📘 Rule violation ⚙ Maintainability ⭐ New
Description
A newly-added comment block is lengthy and partly narrates implementation details, reducing
readability and increasing maintenance burden. Comments should be minimal and focused on non-obvious
rationale only.
Code

src/Capacitor.Cli/Commands/SetupCommand.cs[R943-946]

+        // Built through the ONE authenticated-client choke point, so the bearer is resolved against
+        // this server (refreshing if expired, binding-checked) and a mid-poll 401 is recovered by
+        // refresh — a short-lived WorkOS token cannot turn the back half of a thirty-minute wait into
+        // a dead sign-in. Nothing in the leg throws: the client degrades and the flow answers with a
Evidence
PR Compliance ID 26 requires comments to be minimal and focused on rationale rather than narrating
behavior. The added multi-line comment at the cited location is longer than necessary and includes
narrative detail (e.g., explaining multiple internal mechanics in prose).

CLAUDE.md: Keep Code Comments Minimal, Value-Add Only, and Focused on 'Why' (Including Config/YAML)
src/Capacitor.Cli/Commands/SetupCommand.cs[943-949]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A comment block in `RunBrowserFlowStepAsync` is longer than necessary and includes narrative/implementation detail; per the project standard, comments should be minimal and focused on the non-obvious “why”.

## Issue Context
This comment is in a frequently-read CLI command path; keeping comments short improves long-term maintainability.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/SetupCommand.cs[943-949]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Poll runs past budget ✓ Resolved 🐞 Bug ☼ Reliability ⭐ New
Description
The loop checks its deadline before sleeping but does not check it again after
WaitForIntervalAsync, so an interval that crosses the deadline is followed by another network
poll. That request can extend the nominal 30-minute backstop by the remaining backoff plus the HTTP
timeout.
Code

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[R125-126]

+            if (!first && await WaitForIntervalAsync(interval, last, ct))
+                return new FirstRunFlowResult.Dismissed(last);
Evidence
The only deadline condition is evaluated at loop entry; the interval helper sleeps the full
interval, after which the code invokes PollAsync without another deadline check. The configured
request timeout can add further elapsed time after the budget is already exhausted.

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[111-130]
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[197-207]
src/Capacitor.Cli/Commands/SetupCommand.cs[924-925]

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 polling loop can issue one additional network request after its 30-minute deadline because the deadline is checked before the interval wait, not after it.

## Issue Context
Backoff intervals can reach 30 seconds and each HTTP request has its own 15-second timeout. Bound the delay to the remaining budget or recheck the clock immediately after the delay and return `Abandoned` before calling `channel.PollAsync`.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[111-130]
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[197-207]
- test/Capacitor.Cli.Core.Tests.Unit/FirstRun/BrowserFirstRunFlowTests.cs[403-415]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (2)
6. Retry-After date ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
FirstRunFlowClient only reads Retry-After as a delta, so servers that send Retry-After as an HTTP
date will be treated as having no Retry-After and will fall back to the hardcoded 10-minute default.
This can misreport when browser setup will be available again.
Code

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[R52-55]

+            if (!resp.IsSuccessStatusCode)
+                return new((int)resp.StatusCode, null, resp.Headers.RetryAfter?.Delta);
+
+            return new((int)resp.StatusCode, await ReadAsync(resp, ct));
Evidence
The new code only reads the delta form of Retry-After; the codebase already has a helper that
handles both delta and date forms, showing this omission will lead to incorrect behavior when
date-form headers are used.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[52-55]
src/Capacitor.Cli/SessionStartMemory/SessionStartContextFetch.cs[76-83]
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[77-79]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FirstRunFlowClient.CreateAsync` only uses `resp.Headers.RetryAfter?.Delta`. If the server uses the date form (`RetryAfter.Date`), the value is ignored and the caller will fall back to a default (10 minutes), producing misleading messaging.

## Issue Context
There is already repo precedent for correctly parsing both delta and date forms.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]

### Implementation notes
- When building `FirstRunCreateOutcome` for non-success responses, compute RetryAfter roughly like:
 - `var retryAfter = resp.Headers.RetryAfter?.Delta;
    if (retryAfter is null && resp.Headers.RetryAfter?.Date is { } date) {
       var v = date - DateTimeOffset.UtcNow;
       retryAfter = v > TimeSpan.Zero ? v : null;
    }`
- Keep `RetryAfter` null when absent/unparseable so the higher layer’s fallback remains intact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Verbose docblocks in FirstRun ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Several newly-added comment blocks are overly long and include historical narrative (e.g., retired
pairing/spec references) that reduces readability and exceeds the “minimal, non-obvious rationale”
standard. This increases maintenance cost by burying the intent in multi-paragraph prose instead of
concise constraints.
Code

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[R8-11]

+/// <para><b>Create-then-redirect, and that order is the whole point.</b> The browser then arrives at a
+/// flow that already has an owner, so the server's ownership check has something to check from the
+/// first request rather than from whenever a browser happens to turn up. Reversed, the first browser
+/// to open the link owns the flow — which is where it sat under the retired pairing, and is the one
Evidence
PR Compliance ID 25 requires comments to be minimal and provide only non-obvious rationale. The
added multi-paragraph XML/doc comments in the new FirstRun flow and Setup command include extended
narrative/historical context, making them unnecessarily verbose under this rule.

CLAUDE.md: Code Comments Must Be Minimal and Provide Non-Obvious Rationale (No Restating Code or Narrating Changes)
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added comments are overly verbose and include historical/narrative detail rather than minimal, non-obvious rationale.

## Issue Context
Rule requires comments to be short and focused on important rationale/constraints, not long narration.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
- src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This push adds substantial, independently failure-prone logic across authenticated client setup, flow creation/polling/backoff, keyboard cancellation, and CLI integration, making redundant review materially useful.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 13c6f4b

Results up to commit 558af1b ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Cancellation swallowed in client ✓ Resolved 🐞 Bug ☼ Reliability
Description
FirstRunFlowClient treats OperationCanceledException as a transient transport blip and converts it
to StatusCode=0, which prevents CancellationToken cancellation (e.g., Ctrl+C / host shutdown) from
aborting browser setup polling. This can leave setup stuck polling until the 30-minute PollBudget
instead of stopping promptly on cancellation.
Code

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[R88-89]

+    static bool IsTransient(Exception e) =>
+        e is HttpRequestException or OperationCanceledException or JsonException or NotSupportedException;
Evidence
The new client explicitly includes OperationCanceledException in its transient filter, so any
cancellation (including caller-requested) is flattened into StatusCode=0. In contrast, existing
polling flows in the repo only treat OperationCanceledException as transient when the caller token
was not canceled, or they rethrow when the caller token is canceled.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[61-74]
src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[88-89]
src/Capacitor.Cli.Core/Auth/OAuthLoginFlow.cs[211-219]
src/Capacitor.Cli.Core/Config/ServerUrlNormalizer.cs[114-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FirstRunFlowClient` currently classifies `OperationCanceledException` as transient and degrades it to `StatusCode = 0`. This swallows legitimate caller cancellation (`ct.IsCancellationRequested == true`), which means higher-level cancellation cannot stop the browser setup flow promptly.

## Issue Context
Elsewhere in the codebase, cancellation is explicitly preserved (either rethrown or only treated as transient when the *caller token* is not canceled).

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-74]
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[76-90]

### Implementation notes
- Update `CreateAsync` / `PollAsync` / `ReadAsync` to **rethrow** `OperationCanceledException` when `ct.IsCancellationRequested` is true.
- Only degrade `OperationCanceledException` to status 0 when it represents a timeout or other non-caller cancellation (i.e., `!ct.IsCancellationRequested`).
- One simple pattern:
 - `catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return new(0, null); }`
 - `catch (HttpRequestException) { ... }` etc.
- Remove `OperationCanceledException` from the generic `IsTransient(Exception e)` helper, or replace the helper with overloads that can examine `ct`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Retry-After date ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
FirstRunFlowClient only reads Retry-After as a delta, so servers that send Retry-After as an HTTP
date will be treated as having no Retry-After and will fall back to the hardcoded 10-minute default.
This can misreport when browser setup will be available again.
Code

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[R52-55]

+            if (!resp.IsSuccessStatusCode)
+                return new((int)resp.StatusCode, null, resp.Headers.RetryAfter?.Delta);
+
+            return new((int)resp.StatusCode, await ReadAsync(resp, ct));
Evidence
The new code only reads the delta form of Retry-After; the codebase already has a helper that
handles both delta and date forms, showing this omission will lead to incorrect behavior when
date-form headers are used.

src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[52-55]
src/Capacitor.Cli/SessionStartMemory/SessionStartContextFetch.cs[76-83]
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[77-79]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FirstRunFlowClient.CreateAsync` only uses `resp.Headers.RetryAfter?.Delta`. If the server uses the date form (`RetryAfter.Date`), the value is ignored and the caller will fall back to a default (10 minutes), producing misleading messaging.

## Issue Context
There is already repo precedent for correctly parsing both delta and date forms.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs[39-58]

### Implementation notes
- When building `FirstRunCreateOutcome` for non-success responses, compute RetryAfter roughly like:
 - `var retryAfter = resp.Headers.RetryAfter?.Delta;
    if (retryAfter is null && resp.Headers.RetryAfter?.Date is { } date) {
       var v = date - DateTimeOffset.UtcNow;
       retryAfter = v > TimeSpan.Zero ? v : null;
    }`
- Keep `RetryAfter` null when absent/unparseable so the higher layer’s fallback remains intact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Verbose docblocks in FirstRun ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Several newly-added comment blocks are overly long and include historical narrative (e.g., retired
pairing/spec references) that reduces readability and exceeds the “minimal, non-obvious rationale”
standard. This increases maintenance cost by burying the intent in multi-paragraph prose instead of
concise constraints.
Code

src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[R8-11]

+/// <para><b>Create-then-redirect, and that order is the whole point.</b> The browser then arrives at a
+/// flow that already has an owner, so the server's ownership check has something to check from the
+/// first request rather than from whenever a browser happens to turn up. Reversed, the first browser
+/// to open the link owns the flow — which is where it sat under the retired pairing, and is the one
Evidence
PR Compliance ID 25 requires comments to be minimal and provide only non-obvious rationale. The
added multi-paragraph XML/doc comments in the new FirstRun flow and Setup command include extended
narrative/historical context, making them unnecessarily verbose under this rule.

CLAUDE.md: Code Comments Must Be Minimal and Provide Non-Obvious Rationale (No Restating Code or Narrating Changes)
src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added comments are overly verbose and include historical/narrative detail rather than minimal, non-obvious rationale.

## Issue Context
Rule requires comments to be short and focused on important rationale/constraints, not long narration.

## Fix Focus Areas
- src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs[5-18]
- src/Capacitor.Cli/Commands/SetupCommand.cs[920-933]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/FirstRunFlowClient.cs Outdated
… flow client

Qodo review findings on the first-run browser leg:
- caller cancellation was swallowed as a transport blip, so Ctrl-C could not
  stop the poll before its 30-minute budget; rethrow OCE when the caller token
  is cancelled and degrade only HttpClient's own timeout (same exception type,
  token unsignalled)
- Retry-After was read as delta-seconds only; a proxy rewriting it as an HTTP
  date was reported as no header at all. Read both forms, measured against the
  response's own Date header so server clock skew cannot turn the wait negative
- trim the retired-pairing narrative from the new docblocks down to the
  non-obvious constraints, per the comment rule
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs
Comment thread README.md Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/FirstRunFlowPoll.cs Outdated
@kurrent-io kurrent-io deleted a comment from alexeyzimarev Aug 24, 2026

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peer review via kcap code-review flow (reviewer vendor: claude)

Two rounds, reviewer signed off at round 2. The 8 findings below are those still open at sign-off — nothing fixed or superseded in round 1 is repeated here. Each is posted as an inline thread so it can be resolved individually.

Verification scope — please read before acting. The reviewer's worktree was on the merge-base (a5d92c4d), not the PR branch, so the inlined diff was authoritative for the new files. That means:

  • Verified against the real checkout: every collaborator the leg leans on — HttpClientExtensions, TokenStore, SystemBrowser, IKeyWatcher, and SetupCommand's existing login/ping legs. Findings 1, 2, 3, 5 and 8 are anchored in those, and I independently re-confirmed the citations behind 1, 2 and 3.
  • Not verified: the six test files, the README hunk, and help-setup.txt's rendered result. Test-coverage remarks are flagged as unverified rather than asserted.
  • Could not be settled: the server contract for /api/first-run/flows (absent from both this repo and kcap-server's main). Finding 4 is a contract risk, not a confirmed defect — it needs a check against a real payload.

Ranking: 1-3 are the ones worth acting on before merge. 4 is a should-confirm. 5-8 are cheap fixes; 6 and 8 are explicitly not blockers.

Explicitly not findings, for the record: the create-before-open ordering, composing the setup URL locally rather than taking it from the server, the closed-set outcome mapping, and treating the payload as outcomes-never-instructions all look right. Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(16)) is a correct 22-char/128-bit id. No AOT/trimming issue found: both new models are registered on CapacitorJsonContext (Models.cs:962-963), every serialize/deserialize goes through a JsonTypeInfo, and Base64Url/Enum.ToString/the LINQ All are all AOT-safe.

Of the author's two flagged-for-review decisions: the outcomes-never-instructions boundary holds up. The 401/403-on-create trade does not fully — see Finding 1 for why the premise weakens once the token is read raw.

Comment thread src/Capacitor.Cli/Commands/SetupCommand.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli/Commands/SetupCommand.cs Outdated
/// gate blocks finishing, and a flow whose import failed is over, not stuck.</para>
/// </summary>
public static bool IsFinished(FirstRunFlowResponse view) =>
view.CanFinish && KnownSteps.All(step => IsSettled(view, step));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 4 — IsFinished requires the Done step to carry an outcome; wire-name mapping is exact-case.

(Flagged unverified: the server routes are in neither this checkout nor /Users/tony/dev/kcap-server's main, so this is a contract risk the reviewer could not settle. Please confirm against a real payload.)

Done reads like the terminal value of the step field ("The step the browser is on"), not a gate with its own outcome. If the server's steps dictionary only carries the three gates, StatusOf(view, Done) falls to Pending forever, IsFinished is never true, and a browser flow that completed in ten seconds still burns the full 30-minute budget before reporting Abandoned -> "The browser didn't finish setup." — the exact opposite of what happened.

Same silent-never-finishes failure from a second cause: Step()/Outcome() switch on exact PascalCase wire strings, and StatusOf looks up step.ToString() in a default (ordinal, case-sensitive) Dictionary. A server serializing its step/outcome enums camelCase ("signIn", "completed") or snake_case makes every outcome unrecognised -> Pending -> same 30-minute hang. The "unknown members are dropped" boundary is right, but it means a naming mismatch degrades to a half-hour wait rather than to anything diagnosable.

Fix: (a) confirm against the real payload that Done is always present in steps once can_finish is true, and (b) add at least one test that feeds a recorded server response through IsFinished rather than a hand-built FirstRunFlowResponse — a hand-built fixture agrees with the CLI's own casing by construction and cannot catch this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the server half (the paired kcap-server change): FirstRunFlowView.From always sends all four steps — FirstRunSteps.InOrder (SignIn, Agents, Import, Done) — each with an outcome, keyed by enum.ToString(), so the exact-case names (Done) match this mapping. Done does settle: the payoff screen writes the completed event, and the poll endpoint own comment describes a CLI poll landing "just after the browser settled Done" — the exact state this IsFinished exists to detect. The only forward-compat caveat is the deliberate, documented closed-set trade (an unknown new-server step is dropped rather than stalling the poll). No change needed.

Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs
Comment thread src/Capacitor.Cli/Commands/SetupCommand.cs Outdated
From Alexey's inline comments and Tony's kcap peer review:

- the leg now builds its client through the ONE authenticated-client choke
  point: the bearer is resolved against this server (refreshing if expired,
  binding-checked) and a mid-poll 401 is recovered by refresh, so a
  short-lived WorkOS token cannot turn the back half of a thirty-minute wait
  into a dead sign-in. The token read moved inside the leg's guarded try
  (the "cannot crash setup" promise now covers it), and a non-Ok auth status
  gets one line telling the user to re-login
- the poll verifies the echoed flow_id exactly as the create path does
- the escape hatch stays responsive: the delay is slept in 200ms slices, a
  keypress during an in-flight poll is noticed right after it, and a keypress
  that preceded the wait is drained rather than taken as a dismiss
- the poll backs off on every unhappy response (honouring the route's
  Retry-After) and snaps back to the 2s cadence on a good state
- an unreadable 2xx create body is reported as unreadable, not as a rejection
  quoting the success status
- the setup URL is reprinted every ~minute so the poll dots cannot scroll the
  one line a headless machine's user needs to read away
- docs: the browser leg's skip list now includes auth provider None
- fix the stale rationale on the poll 401 classification
@alexeyzimarev
alexeyzimarev self-requested a review August 24, 2026 13:33
alexeyzimarev
alexeyzimarev previously approved these changes Aug 24, 2026
Comment thread src/Capacitor.Cli/Commands/SetupCommand.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e3a0ae7

- a server-provided Retry-After is honoured as-is, even beyond the 30s cap
  that still bounds the locally computed doubling; a rate-limited route that
  asks for 60s is not polled at 30s
- the stale-input drain moved to before the "press any key" prompt renders:
  a key that preceded the leg is still drained, and a key pressed in response
  to the prompt is a real dismissal, not stale input
- the poll loop re-checks the budget deadline after the interval wait, so a
  sleep crossing the deadline ends the wait instead of issuing one more poll
- trim the choke-point comment in the leg down to the non-obvious why

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of the latest head found two actionable issues. The first allows a server-supplied delay to defeat the flow's stated 30-minute backstop; the second exposes raw Spectre tags in an error path. No build or tests were run, per request.

Comment thread src/Capacitor.Cli.Core/FirstRun/BrowserFirstRunFlow.cs Outdated
Comment thread src/Capacitor.Cli/Commands/SetupCommand.cs
…ough Spectre

From Tony's static review of the latest head:
- a server Retry-After longer than what remains of the 30-minute budget no
  longer sleeps past the backstop: the interval is capped at deadline - now,
  so a route that asks for an hour cannot hold a keyboard-less host for one
- the no-token skip line goes through AnsiConsole so its [dim] markup renders
  instead of printing literally
@realtonyyoung

Copy link
Copy Markdown
Collaborator

NO FINDINGS

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the latest head (13c6f4b). The previously reported findings are addressed. No build or tests were run, per request.

@George-Payne
George-Payne merged commit 283b3a4 into main Aug 24, 2026
6 checks passed
@George-Payne
George-Payne deleted the georgepayne/ai-2156-setup-creates-and-polls-flow branch August 24, 2026 14:39
Coldaine pushed a commit to MooseGooseConsulting/kcap-cli that referenced this pull request Aug 25, 2026
…rent-io#640)

* Create the first-run flow before opening the browser, and poll it

The server's two rendezvous routes shipped with no caller at all, so nothing
generated a flow id and the browser's claim-on-arrival was what established
ownership - which is where it sat under the retired pairing, and the one
property of the design the server half could not realise alone.

The leg runs after login, since both routes are authenticated. It generates a
128-bit base64url id, creates the flow, opens {server}/setup?s=<id>, and polls
until every step it knows has settled.

- Refusals are handled apart: 404/401/403/405 on the create mean the tenant
  does not serve the flow, and say nothing; 429 reports the server's own
  Retry-After rather than sleeping through it; 409 retries with a fresh id,
  since it means the id is taken rather than the credentials wrong.
- The poll's decision is extracted and unit-tested per branch. 410 is a dead
  link, 404 a flow that will never be ours, 401 a re-login rather than a new
  link, and 5xx or a transport blip is another tick.
- Outcomes, never instructions. Step and status strings map onto closed local
  sets and an unrecognised member is dropped, because kcap setup writes Claude
  Code hooks and a hook entry is a command string Claude Code runs. Which steps
  are gates stays the server's to say, via can_finish.
- The setup URL is composed locally, so unlike the pairing there is no
  server-supplied URL reaching a shell-executed open to validate.
- Any key ends the wait. The 30-minute budget is the backstop for a terminal
  nobody is sitting at; a closed tab should not cost half an hour of dots.
- Headless is deliberately not a skip - the link is printed as well as opened,
  which is what keeps the screens available to the device-path population.

The leg reports and configures nothing: the screens that would push
configuration are their own tickets, and the terminal steps remain what wires
the machine up.

* Honour caller cancellation and date-form Retry-After in the first-run flow client

Qodo review findings on the first-run browser leg:
- caller cancellation was swallowed as a transport blip, so Ctrl-C could not
  stop the poll before its 30-minute budget; rethrow OCE when the caller token
  is cancelled and degrade only HttpClient's own timeout (same exception type,
  token unsignalled)
- Retry-After was read as delta-seconds only; a proxy rewriting it as an HTTP
  date was reported as no header at all. Read both forms, measured against the
  response's own Date header so server clock skew cannot turn the wait negative
- trim the retired-pairing narrative from the new docblocks down to the
  non-obvious constraints, per the comment rule

* Address peer-review findings on the first-run browser leg

From Alexey's inline comments and Tony's kcap peer review:

- the leg now builds its client through the ONE authenticated-client choke
  point: the bearer is resolved against this server (refreshing if expired,
  binding-checked) and a mid-poll 401 is recovered by refresh, so a
  short-lived WorkOS token cannot turn the back half of a thirty-minute wait
  into a dead sign-in. The token read moved inside the leg's guarded try
  (the "cannot crash setup" promise now covers it), and a non-Ok auth status
  gets one line telling the user to re-login
- the poll verifies the echoed flow_id exactly as the create path does
- the escape hatch stays responsive: the delay is slept in 200ms slices, a
  keypress during an in-flight poll is noticed right after it, and a keypress
  that preceded the wait is drained rather than taken as a dismiss
- the poll backs off on every unhappy response (honouring the route's
  Retry-After) and snaps back to the 2s cadence on a good state
- an unreadable 2xx create body is reported as unreadable, not as a rejection
  quoting the success status
- the setup URL is reprinted every ~minute so the poll dots cannot scroll the
  one line a headless machine's user needs to read away
- docs: the browser leg's skip list now includes auth provider None
- fix the stale rationale on the poll 401 classification

* Address the follow-up qodo review on the first-run browser leg

- a server-provided Retry-After is honoured as-is, even beyond the 30s cap
  that still bounds the locally computed doubling; a rate-limited route that
  asks for 60s is not polled at 30s
- the stale-input drain moved to before the "press any key" prompt renders:
  a key that preceded the leg is still drained, and a key pressed in response
  to the prompt is a real dismissal, not stale input
- the poll loop re-checks the budget deadline after the interval wait, so a
  sleep crossing the deadline ends the wait instead of issuing one more poll
- trim the choke-point comment in the leg down to the non-obvious why

* Bound the poll wait by the remaining budget; render the skip line through Spectre

From Tony's static review of the latest head:
- a server Retry-After longer than what remains of the 30-minute budget no
  longer sleeps past the backstop: the interval is capped at deadline - now,
  so a route that asks for an hour cannot hold a keyboard-less host for one
- the no-token skip line goes through AnsiConsole so its [dim] markup renders
  instead of printing literally
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants