Skip to content

DEV-1427: docs: add profile management to README - #8

Merged
alexeyzimarev merged 2 commits into
mainfrom
capacitor/agent-3e20645c196d41
Apr 11, 2026
Merged

alexeyzimarev merged 2 commits into
mainfrom
capacitor/agent-3e20645c196d41

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Summary

  • Add documentation for kapacitor profile commands (add, list, show, remove) and kapacitor use (switching profiles with --global and --save flags)
  • Document the profile resolution order (CLI flag > env vars > .kapacitor.json > remote matching > bindings > default)
  • Add whoami and login to the "Other commands" section

Closes #7

Test plan

  • Verify all documented commands match actual CLI help text
  • Confirm profile resolution order matches ProfileResolver.cs implementation

🤖 Generated with Claude Code

Closes #7

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add profile management and authentication commands to README

📝 Documentation

Grey Divider

Walkthroughs

Description
• Add comprehensive profile management documentation
• Document profile creation, listing, and switching commands
• Explain profile resolution order and priority hierarchy
• Add whoami and login commands to "Other commands" section
Diagram
flowchart LR
  A["Profile Management"] --> B["Create/List/Show/Remove"]
  A --> C["Switch Profiles"]
  C --> D["Local Binding"]
  C --> E["Global Default"]
  C --> F["Team Sharing"]
  G["Profile Resolution"] --> H["CLI Flag"]
  H --> I["Environment Variables"]
  I --> J["Config File"]
  J --> K["Remote Patterns"]
  K --> L["Repo Binding"]
  L --> M["Default Profile"]
  N["Auth Commands"] --> O["whoami"]
  N --> P["login"]
Loading

Grey Divider

File Changes

1. README.md 📝 Documentation +38/-0

Add profile management and auth documentation

• Add new "Profiles" section with commands for profile management (add, list, show, remove)
• Document profile switching with kapacitor use command and flags (--global, --save)
• Add detailed profile resolution order explaining priority hierarchy from CLI flags to default
 profile
• Add whoami and login commands to "Other commands" section

README.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Remediation recommended

1. Repo root scope mismatch ☑ 🐞 ≡
Description
The README says profile resolution checks .kapacitor.json “in the repo root” and describes
bindings as “repo-specific”, but the implementation treats the current directory as the repo root
when not inside a git repo. This can mislead users because .kapacitor.json (and kapacitor use
bindings) in a non-repo directory still affect profile selection.
Code

README.md[R182-184]

+4. `.kapacitor.json` in the repo root
+5. Git remote pattern matching from `--remote` flags
+6. Repo-specific binding from `kapacitor use`
Evidence
AppConfig.RepoRoot falls back to Environment.CurrentDirectory when git rev-parse fails, and
.kapacitor.json is read from Path.Combine(repoRoot, ".kapacitor.json"). kapacitor use also
binds and optionally writes .kapacitor.json using AppConfig.RepoRoot, so the behavior applies to
non-repo directories too, not only git repo roots.

README.md[173-185]
src/kapacitor/Config/AppConfig.cs[46-46]
src/kapacitor/Config/AppConfig.cs[78-83]
src/kapacitor/Commands/UseCommand.cs[13-20]
src/kapacitor/Commands/UseCommand.cs[46-55]

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

### Issue description
README’s profile resolution order implies `.kapacitor.json` and bindings only apply at a git repo root, but the CLI intentionally falls back to the current directory when not in a git repo.

### Issue Context
- `AppConfig.RepoRoot` returns `GetGitRepoRoot() ?? Environment.CurrentDirectory`.
- Resolution reads `.kapacitor.json` from that `RepoRoot`.
- `kapacitor use` binds (and with `--save` writes `.kapacitor.json`) using the same `RepoRoot`, so behavior is directory-scoped outside git.

### Fix Focus Areas
- README.md[175-185]

### Suggested change
Update bullets (4) and (6) to match behavior, e.g.
- “`.kapacitor.json` in the repo root (or current directory if not in a repo)”
- “Directory/repo binding from `kapacitor use`” (or similar wording consistent with `help-use.txt`).

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@alexeyzimarev alexeyzimarev changed the title docs: add profile management to README DEV-1427: docs: add profile management to README Apr 11, 2026
@linear

linear Bot commented Apr 11, 2026

Copy link
Copy Markdown

…it repos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit d9dca61 into main Apr 11, 2026
3 checks passed
@alexeyzimarev
alexeyzimarev deleted the capacitor/agent-3e20645c196d41 branch April 11, 2026 14:50
realtonyyoung added a commit that referenced this pull request Jul 17, 2026
…byte checkpoint, heartbeat-aware idle ceiling, delivery-boundary re-checks

Addresses findings #2, #3, #6, and the watcher-side half of #8/#1 from
kcap-cli PR #324 review:

- #2: CursorRewriteGuard.VerifyFullPrefix existed but was never called from
  the poll loop; wire it in on a periodic cadence (CursorFullPrefixVerifyEveryNPolls).
  Also add VerifyNotShrunk — the guard's zone checks were previously gated
  entirely behind "did the file grow", so a shrink or in-place same-length
  rewrite slipped through undetected.
- #3: checkpoint the Cursor byte cursor using the SAME capped snapshot length
  ReadNewCompleteLinesAsync sampled (NewTranscriptLines.SnapshotByteLength),
  not a fresh FileInfo.Length re-sample racing a concurrent append. Advance
  the checkpoint only as far as the server's acked LINE count actually
  covers (ByteOffsetForAckedLines), not the full capped range, so a
  partially-disposed D3 batch never has its unacked tail checkpointed as
  delivered.
- #6: the Cursor idle clock (ShouldEndOnIdle) is now the later of transcript
  activity and the hook heartbeat mtime (ResolveCursorIdleClock), and child
  (subagent) watchers are idle-ceiling eligible without requiring
  ThresholdReached, which they never set.
- #8 (watcher half): re-check the quarantine/barrier markers immediately
  before SendTranscriptBatchAcked, not only at the top of the poll.
- #1 (backfill half): CursorTranscriptBackfill re-checks quarantine/barrier
  immediately before the POST, closing the same race window on that path.

DrainNewLines is now internal (was private) so the guard wiring is directly
regression-testable without a live SignalR server — every path exercised
trips before ever touching the HubConnection argument.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alexeyzimarev pushed a commit that referenced this pull request Jul 18, 2026
* Fix Kiro parent-PID watchdog name match (kiro-cli)

Adds a bounded `-cli` suffix tolerance to MatchesAgentName so the by-name
ancestry walk identifies the durable `kiro-cli` process for vendor `kiro`,
instead of falling back to the fragile getpgrp/getppid heuristic.

Refs AI-1359.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Freeze the watcher idle clock while disconnected

Subtracts SignalR-outage time (accrued since last activity) from the idle
measure so a transient disconnect can't false-idle-end a Codex/Antigravity
session, while repeated reconnects with no new lines still idle-end after
the connected budget.

Refs AI-1359.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add staged parent-dead / wedged-watcher recovery

On ParentAlreadyDead the watcher now periodically re-resolves + re-arms the
parent-exit watchdog (using the vendor process-name alias); it ends the
session only after a long, configurable ceiling with no transcript progress
and continued resolution failure, and any new progress resets the window.
This is the only end path for a wedged, alive-but-connected watcher that the
server stale sweep can't see. New env var KCAP_PARENT_DEAD_CEILING_MINUTES.

Refs AI-1359.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: AgentHookPoster.Spooled + PostOrSpoolAsync spool-on-lapse

Adds a Spooled outcome and PostOrSpoolAsync: on lapsed-auth or transient
(5xx/408/429/unreachable) failure the lifecycle payload is durably spooled
(HookSpool) for a later drain pass, so live capture can start regardless of
hook-POST delivery (spawn-before-post). Refs AI-1357.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: TranscriptSpool — bounded, no-drop, needs-import marker

AI-1357 Task 2: dedicated on-disk spool for undelivered transcript-tail
batches captured during a watcher outage. Unlike HookSpool (1 MB,
drop-oldest — fine for small lifecycle POSTs), this is bounded at 8 MB
per session with NO SILENT DROP: on cap exhaustion it stops appending
and writes a needs-import marker instead of truncating history, so the
session surfaces as requiring `kcap import` rather than silently losing
transcript content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: TranscriptSpool no silent drop on I/O failure + Ignored result

Review fixes for AI-1357 Task 2:
- Append now marks needs-import (not phantom Appended) when the live
  write throws — the silent-drop this class exists to prevent.
- MarkNeedsImport returns bool and logs to stderr on failure so callers
  don't trust an unpersisted marker.
- Add AppendResult.Ignored for the malformed-session-id drop so it can't
  be mistaken for a real append.
- Tests for the I/O-failure path and the malformed-id path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: LifecycleSpoolDrain — global cross-spool ordered drain pass

Adds a session-agnostic drain pass to run at the start of every kcap
invocation (and periodically by the daemon), enforcing per-session
ordering across the lifecycle (HookSpool) and transcript (TranscriptSpool)
spools: spooled session-start -> transcript tail (+ needs-import marker,
delivered even over cap) -> spooled session-end. Adds the route-filtered
HookSpool.DrainRoutesAsync/SessionIdsWithBacklog and
TranscriptSpool.SessionIdsWithBacklog helpers this needs; HookSpool's
existing route-agnostic DrainAllAsync is untouched (still used by
Claude/Cursor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: spawn-before-post + Spooled for Kiro/OpenCode/Pi/Copilot hooks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: throttle spool drain per-prompt; ShouldSpawnAfter drops AuthLapsed

AI-1357 review: (1) DrainSpoolsAsync now self-throttles via an on-disk
.last-drain stamp (30s) so Kiro agentSpawn / OpenCode idle re-fires can't
attempt a network drain every prompt during an outage; reaps moved inside.
(2) ShouldSpawnAfter spawns only on Posted/Spooled — AuthLapsed spools
nothing, so spawning would orphan a session with a dropped SessionStarted.
(3) Documented the fresh-client deviation (no reusable vendor client exists
at the pre-POST drain point).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Codex spawn-before-post with stdout-first carve-out

Auth lapse now spools + spawns via PostOrSpoolAsync/ShouldSpawnAfter
(matching Kiro/OpenCode/Pi/Copilot) instead of dropping the session and
skipping the watcher. The global lifecycle/transcript spool drain runs
fire-and-forget AFTER Codex's blocking `{"continue":true}` stdout
handshake — never before, never gating it — so a large/unreachable spool
backlog can't stall the parent process. Extracted WriteSessionScopedOutput
and RunSessionStartHandshakeForTest as the seam CodexStdoutContractTests
uses to prove the ordering without a live server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Antigravity spawn-before-post + Gemini awaited EnsureWatcherRunning

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Gemini session-start spawn-before-post + gated spool drain

Migrate Gemini's session-start off the POST-only PostAsync path to
AgentHookPoster.PostOrSpoolAsync + ShouldSpawnAfter, so an auth lapse or
transient outage spools the SessionStarted payload and STILL spawns the
watcher (was: returned without reaching EnsureWatcherRunning on anything
but Posted). Wire the throttled DrainSpoolsAsync gated to the two lifecycle
events (SessionStart/SessionEnd) so the per-turn Notification path adds no
network cost. Fix a dangling "PostHookAsync (below)" comment in
AntigravityHookCommand left by the Task 6 deletion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: shutdown final-line completion signal (newline/parseable-JSON)

The shutdown-during-outage final drain (idle-timeout / parent-exit) used
to disable the half-written-line holdback unconditionally, which could
consume a line the agent was still mid-write on, or drop a complete
newline-less final line. Add a pure IsFinalLineComplete signal (empty /
newline-terminated / last line parses as JSON — length-stability alone is
NOT proof, since a large write can pause mid-record past any bounded
window) plus a bounded (<=2s) WaitForFinalLineCompletionAsync wait in the
final-drain path. Complete -> send the newline-less final line as before;
incomplete -> keep the holdback on (never send-and-advance a truncated
line) and flag the session needs-import via TranscriptSpool so `kcap
import` can recover it later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: close final-drain completeness TOCTOU; re-validate at consume time

Review found the completeness decision and the consuming read were two
separate reads: WaitForFinalLineCompletionAsync probed once, then
DrainNewLines consumed with holdback disabled — so a final line that
resumed growing in the gap was sent-and-advanced anyway, violating "never
consume a still-growing line."

Move the decision into the consuming read via an IncompleteFinalLinePolicy
(Hold / ConsumeIfComplete). Under ConsumeIfComplete the unterminated final
line is consumed ONLY IF the exact bytes read parse as a complete JSON
record; otherwise it is held and NewTranscriptLines.HeldIncompleteFinalLine
is set. The parse check runs on the same bytes being consumed, so there is
no TOCTOU with the bounded pre-wait (now purely advisory — gives the writer
time). RunWatch flags needs-import off the actual consume-time held result
via state.FinalDrainHeldIncompleteLine.

Also updates the stale ApplyPartialLineHoldback comment that claimed the
final drain opts out of holdback because "the file is static then" — the
exact wrong assumption this task fixes.

Tests: SplitNewCompleteLines + ReadNewCompleteLinesAsync gain parseable-
consume / unparseable-held cases, HeldIncompleteFinalLine parity, and a
grew-after-the-completeness-check TOCTOU guard asserting the resumed
partial is held, not sent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: spool undelivered transcript tail on shutdown-during-outage

At final drain during idle-timeout/parent-exit shutdown, DrainNewLines only
advances LinesProcessed past lines the hub confirmed sent. If the hub is
down (or still reconnecting) at that point, any transcript lines from
LinesProcessed to EOF were never delivered and the process exits right
after — silently dropping the tail.

Adds BuildTranscriptSpoolBatch (pure TranscriptBatch JSON builder) and
SpoolUndeliveredTranscriptTailAsync, which re-reads the undelivered tail
using the same ConsumeIfComplete completion decision as the final drain
and spools it into the dedicated TranscriptSpool (task 2) so the global
drain (task 3) replays it after recovery, without a manual `kcap import`.
Cap exhaustion still marks the session needs-import rather than dropping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: spool tail unconditionally + redact secrets (task-8 review)

Two Critical review findings on the shutdown-during-outage tail spool:

1. Insufficient trigger → silent drop. Gating the spool on
   hubConnection.State != Connected missed a HubException thrown by
   SendTranscriptBatch2 while the connection stayed Connected (the generic
   catch does not change connection state) — the tail was undelivered but
   never spooled. Call SpoolUndeliveredTranscriptTailAsync unconditionally
   on the shutdown path; it is already a no-op when position == EOF.

2. SECURITY: spooled tail skipped secret redaction. The live/inline drains
   run lines through SecretRedactor.RedactLine, but the spooled tail was
   written raw → secrets on disk and POSTed unredacted on replay. Redact
   each spooled line exactly as the live drain does before writing.

Tests: undelivered_tail_spooled_even_though_connection_stayed_up (state-
agnostic spooling) and spooled_tail_is_secret_redacted (ghp_ token →
[REDACTED], raw secret absent from disk).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: watcher heartbeat + lock-guarded staleness reap+respawn

AI-1357 task 9. WatcherManager.IsWatcherAlive only checked the PID, so a
wedged (hung-but-alive) watcher was never restarted. The watcher now
touches a per-key heartbeat file every main-loop iteration — including
no-content drains and while disconnected/reconnecting — via the pure
WatcherHeartbeat helper (Core). IsWatcherAlive requires both a live PID
and a non-stale heartbeat (past a 30s startup grace, 20s threshold).

EnsureWatcherRunning reaps a wedged watcher (kill + respawn) under a
cross-platform spawn lock — same FileShare.None/flock primitive as
DaemonLock — so concurrent hooks racing the same key can't double-spawn.
The whole decide-and-spawn sequence is locked, not just the reap: killing
the old watcher deletes its pid file before the respawn's new one lands,
so guarding only the reap step would leave a window where a second hook
sees "no pid" and spawns unguarded (caught by the new concurrency test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: keep heartbeat fresh during connect-retry + purge watcher sidecar files

AI-1357 task 9 review. Two Important issues.

1. Startup/reconnect outage looked "wedged" → false reap. The SignalR
   connect-retry loop never touched the heartbeat, so a server outage
   >=~50s at startup was indistinguishable from a wedged loop and the
   hook probe would repeatedly reap+respawn a healthy-but-reconnecting
   watcher. RunWatch now touches the heartbeat at the top of every
   connect-retry iteration and waits via DelayWithHeartbeatAsync, which
   chunks the backoff (grows to 30s > the 20s threshold) into <=5s slices
   through the new pure HeartbeatSlices helper, touching before each — so
   no wait window can ever cross the staleness threshold. The automatic-
   reconnect path was already covered by the main loop's per-iteration
   touch in the disconnected branch.

2. Sidecar file leak. Only .pid was deleted. KillWatcher now removes the
   {key}.heartbeat and {key}.started markers, but deliberately NOT the
   {key}.spawnlock: KillWatcher runs from inside WithSpawnLock on the reap
   path, and unlinking a held lock file on POSIX lets a racing hook open a
   fresh non-conflicting flock (the DaemonLock unlink-race). Spawn locks
   are swept by the new WatcherManager.PurgeAuxiliaryFiles(), called by
   kcap cleanup (holds no lock; also mops up orphans). CleanupCommand now
   uses GetWatcherDir() so it honors KCAP_WATCHER_DIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Kiro live usage-backfill synthetic lines (credits/context%)

AI-1357 task 10: the live kcap watch path never emitted Kiro's per-turn
credits/context% (the AI-1196 hedge only fires on import, which reads the
sibling {id}.json up front). A live drain has usually already sent the
turn's anchor AssistantMessage line by the time Kiro flushes that sidecar,
so inline enrichment can't reach it there.

Add a synthetic KiroUsageBackfilled line per turn anchor instead, mirroring
the Antigravity synthetic-USAGE-line pattern: WatchCommand.
BuildKiroUsageBackfillLine builds the JSONL; AppendKiroUsageBackfillLines
reads the sidecar via KiroUsage.AnchorMap and appends one line per anchor
not yet in the new WatchState.KiroUsageEmittedAnchors, staging them on
KiroUsagePendingAnchors for the caller to commit only after a successful
send (so a failed batch re-reads and re-stages instead of losing them).
Server-side event fold is a separate follow-up (task 13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Cursor live subagent split via hook/backfill path (AI-1151)

CursorSubagentCorrelator only ran on the historical import path, so a live
Cursor Task/Agent subagent was ingested as its own top-level session instead
of nesting under its parent. Cursor is not watcher-backed (CursorHookCommand
backfills each session's transcript over HTTP as hooks arrive), so the
correlation now runs inline in that per-hook dispatcher via a new thin
wrapper, CursorLiveSubagentLinker.

At a child's sessionStart, ResolveParent (reusing
CursorSubagentCorrelator.Correlate) checks the sibling transcripts under the
same agent-transcripts workspace dir; a match is persisted to a small
on-disk marker (the CLI is a fresh process per hook) and diverts subsequent
hooks for that session: subagent-start replaces the top-level sessionStart,
transcript backfill is routed under the parent with agent_id=child (mirrors
CursorImportSource.SendSubagentLifecycleAsync's watermark + POST shape), and
subagent-stop replaces sessionEnd. Mid-lifecycle hooks
(beforeSubmitPrompt/afterAgentThought/telemetry) are not forwarded for a
linked child, matching the import path (which has no side channel for them
either) instead of writing to a phantom AgentSession stream that never got a
SessionStarted.

Dashless ids are used throughout so a live-then-import of the same session
converges on the same AgentSubsession-{parent}-{child} stream rather than
duplicating it (ties to AI-1358 A1). Known eventual-consistency gap
(documented in code): if the parent's Task tool_use isn't yet flushed to
disk at the child's first hook, the child is temporarily ingested top-level
until a later `kcap import --cursor` converges it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: enforce start-before-stop spool ordering on Cursor live subagent path (AI-1151)

HandleSubagentChildEventAsync posted subagent-stop (and ran the sessionStart
backfill) with no spool.HasBacklog check, unlike the sibling top-level path.
Under a transient failure that left a subagent-start undelivered in the spool,
a later subagent-stop could be POSTed ahead of its own subagent-start, leaving
the AgentSubsession stream mis-ordered / never opened.

Mirror the top-level guard: after the HandleCore drain, if the child still has
spool backlog (drain hit a transient failure), spool the fresh lifecycle event
behind it and skip the agent-routed backfill so the next hook re-drains
start-first. Also gate the sessionStart backfill on the subagent-start POST
succeeding, so the transcript can't be routed to an AgentSubsession the
now-spooled start hasn't opened yet.

Adds a regression test: a child whose subagent-start is spooled (transient
failure) does not get subagent-stop delivered ahead of it, and a later
recovered drain delivers start before stop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: wire global spool-drain pass into hook entry + daemon-periodic

AI-1357 Task 12. Runs LifecycleSpoolDrain centrally in Program.cs's
`case "hook":` (before dispatch, non-Codex; throttled via the existing
AgentHookPoster.DrainSpoolsAsync) instead of per-vendor, and adds a 60s
daemon-periodic sweep (SpoolDrainLoop) for backlogs no later hook process
ever touches (Kiro/OpenCode watcher-owned session-end, GUI idle/parent-exit).

Moves HookSpool/TranscriptSpool/LifecycleSpoolDrain to Capacitor.Cli.Core so
the daemon can share them without referencing the CLI's exe project.

Resolves three ordering hazards surfaced by this wiring (each covered by a
new test): the ordered drain's withheld temp files now use a distinct
".ordered-*" namespace so Claude/Cursor's unrelated FIFO drain can never
cross-consume them (BLOCKER-1); the generic drain now fires the
generate_whats_done side effect on any vendor's session-end, not just
Claude's own poster (BLOCKER-2); and a session whose session-end was already
delivered is durably marked ended so a later straggler entry is dropped
instead of replayed out of order (BLOCKER-3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: Claude ordering guard must see the .ordered-* backlog namespace

AI-1357 Task 12 review fix. ClaudeHookCommand.CurrentSessionHasBacklog was a
stale private duplicate that only checked {sid}.jsonl + {sid}.*.draining, not
the .ordered-* namespace this task introduced. Since the centralized ordered
drain now runs on every non-Codex invocation (incl. --claude), a Claude
session-end withheld in .ordered-* pending the transcript tail was invisible
to the guard, so a later Claude subagent-stop for the same session posted
directly — ahead of the still-withheld session-end (the exact BLOCKER-1/3
cross-spool ordering violation).

Delegate CurrentSessionHasBacklog to the public HookSpool.HasBacklog (which
covers all three namespaces), matching CursorHookCommand. Add a test proving a
subagent-stop spools behind a session-end withheld in .ordered-*.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: AI-1357 CLI acceptance tests (spawn-before-post, drain order, heartbeat)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: add import origin marker constant (CLI)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: shared guarded discovery helper (A4)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: adopt guarded discovery in all import sources (A4)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: restore flat discovery scope for Kiro/Gemini (A4 fix)

GuardedDiscovery.EnumerateFiles gains a recursive flag (default true); the
two originally-TopDirectoryOnly call sites pass recursive:false so A4 adds
symlink/cycle/inaccessible/per-entry safety without widening discovery scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: OpenCode ended_at null/0 not 1970 (A3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: per-vendor ended_at resolvers (A3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: confirm/extend Codex ExtractLastTimestamp shape (A3, open Q2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: Claude/Codex resume end-only reassert + fail-closed tail (A2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: routed sources send historical-import origin marker (A1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: Cursor import no stale PR + per-cwd repo cache (item 5)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* AI-1358: Antigravity import usage pass, USAGE-before-end, all classifications (item 7)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 7: CLI phase-0 append-only harness + runtime rewrite guard + quarantine (D0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 8: CLI side-effect barrier + hook heartbeat (D1)

Adds CursorMarkers.CreateBarrier/BarrierPending/ClearBarrier (thin wrappers
over WatcherHeartbeat's atomic timestamp read/write) and TouchHeartbeat.
CursorHookCommand.HandleCore now touches the per-session heartbeat on every
invocation carrying a session id (including telemetry-only hooks), creates
the barrier before beforeSubmitPrompt's own POST (clearing it on a 2xx from
either that live POST or a later hook-spool drain delivery of the same
spooled entry), and relies on the existing spool-drain-before-transcript-
drain ordering for sessionEnd (already correct — no reorder needed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 9: CLI per-session Cursor watcher spawn, precedence-ordered (D1)

Adds "cursor" to WatchCommand.KnownVendors and the watch --vendor usage
string. CursorHookCommand gains the pure ShouldSpawnWatcher(eventName,
isSubagentChild) precedence predicate (terminal hooks never spawn;
a correlated subagent child never spawns a top-level watcher — routed via
the gated parent-child key in a later task) and MaybeSpawnWatcherAsync,
which additionally gates on the quarantine marker and a non-empty
transcript path before calling WatcherManager.EnsureWatcherRunning(vendor:
"cursor"). Wired into HandleCore: sessionStart spawns before its POST
(reusing the workspace_roots-derived cwd already computed for repository
enrichment); every other non-terminal hook spawns only after its own
lifecycle POST has succeeded (recovery spawn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 10: CLI hook-backfill hardening — shared reader, Hold/ConsumeIfComplete, barrier+quarantine-aware (D2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 11: CLI watcher exit conditions + acked-batch cursor (D1/D3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 12: CLI child subagent watchers gated on acked subagent-start

HandleSubagentChildEventAsync now spawns the child (subagent) Cursor
watcher — key `{parentSessionId}-{childSessionId}`, tailing the child's
own transcript file, agentId=childSessionId/sessionIdOverride=parentSessionId
— only once the diverted subagent-start POST is acknowledged (2xx).
A spooled start (POST failure) defers the spawn entirely: no code path
spawns a child watcher for an unacked start, preserving the invariant
that no child transcript line reaches the server before SubagentStarted
is appended.

The deferred half is wired into HandleCore's generic top-of-method spool
drain (which runs before the isSubagentChild divert, keyed on the same
childSessionId): when it redelivers a previously-spooled subagent-start
entry and gets a 2xx, it parses the parent/child/transcript-path triple
back out of the entry's own payload and performs the spawn then — the
"later invocation whose spool drain delivers the start" the design calls
for. Quarantine is checked on the parent session id, matching how
CursorRewriteGuard/WatchCommand.RunWatch resolve their own guard identity
for a child watcher process (sessionId = sessionIdOverride ?? key).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Task 13: end-to-end acceptance tests + docs (CLI half)

Adds CursorTailingWatcherTests.cs covering the plan's four acceptance
scenarios from the CLI side: force-quit mid-turn (a transcript's final
line, complete but not yet newline-terminated, is held by every live
drain and only consumed by the shutdown final drain); idle-ceiling exit
without Cursor synthesizing its own session-end (extracts the pure
CursorSuppressesEndPost(vendor, idleExit) helper out of RunWatch's
inline check so the suppression decision is independently testable,
contrasted against Codex which does NOT suppress); and reactivation via
BOTH a sessionStart resume and a non-sessionStart resume hook, driven
through the real CursorHookCommand.HandleCore dispatcher rather than
the bare ShouldSpawnWatcher predicate. "Sweep closes" is server-side
(already proven by Task 5's StaleActiveSessionReaperTests) and is out
of this repo's visibility — noted in the test file's doc comment rather
than re-derived.

Also documents the watcher-backed Cursor capture (hooks retained as
belt-and-braces, child-watcher acked-start gating, force-quit/idle-
ceiling/reactivation behavior, the runtime rewrite guard) and the
KCAP_CURSOR_IDLE_CEILING_MINUTES knob in README.md, plus
cursor-verify-appendonly as the phase-0 diagnostic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix: rewrite-guard shrink/full-prefix wiring, acked-byte checkpoint, heartbeat-aware idle ceiling, delivery-boundary re-checks

Addresses findings #2, #3, #6, and the watcher-side half of #8/#1 from
kcap-cli PR #324 review:

- #2: CursorRewriteGuard.VerifyFullPrefix existed but was never called from
  the poll loop; wire it in on a periodic cadence (CursorFullPrefixVerifyEveryNPolls).
  Also add VerifyNotShrunk — the guard's zone checks were previously gated
  entirely behind "did the file grow", so a shrink or in-place same-length
  rewrite slipped through undetected.
- #3: checkpoint the Cursor byte cursor using the SAME capped snapshot length
  ReadNewCompleteLinesAsync sampled (NewTranscriptLines.SnapshotByteLength),
  not a fresh FileInfo.Length re-sample racing a concurrent append. Advance
  the checkpoint only as far as the server's acked LINE count actually
  covers (ByteOffsetForAckedLines), not the full capped range, so a
  partially-disposed D3 batch never has its unacked tail checkpointed as
  delivered.
- #6: the Cursor idle clock (ShouldEndOnIdle) is now the later of transcript
  activity and the hook heartbeat mtime (ResolveCursorIdleClock), and child
  (subagent) watchers are idle-ceiling eligible without requiring
  ThresholdReached, which they never set.
- #8 (watcher half): re-check the quarantine/barrier markers immediately
  before SendTranscriptBatchAcked, not only at the top of the poll.
- #1 (backfill half): CursorTranscriptBackfill re-checks quarantine/barrier
  immediately before the POST, closing the same race window on that path.

DrainNewLines is now internal (was private) so the guard wiring is directly
regression-testable without a live SignalR server — every path exercised
trips before ever touching the HubConnection argument.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix: recovery spawn withheld on spool backlog; dropped subagent-start permanently gates child transcript delivery

Addresses findings #4 and #5 from kcap-cli PR #324 review:

- #4: capture whether the session still has spool backlog AFTER the
  generic top-of-method drain attempt, and require no remaining backlog
  before the recovery-spawn watcher call — a telemetry-only mapping
  (SpoolOnFailure=false) previously reached the spawn regardless of
  whether an earlier canonical event (e.g. sessionStart) was still stuck
  undelivered.
- #5: add a durable per-child subagent-start-acknowledgement marker
  (CursorMarkers.MarkSubagentStartAcked/HasSubagentStartAck), written the
  moment a 2xx is observed (live POST or a later spool-drain delivery).
  HandleSubagentChildEventAsync now gates ALL non-start hooks (content-less
  backfill and the child's own subagent-stop) on this marker instead of on
  "no spool backlog" — a subagent-start that hits a non-transient 4xx on
  retry is permanently Dropped from the spool (HasBacklog goes false) even
  though no AgentSubsession stream was ever opened server-side; without the
  marker that emptied backlog let child transcript content flow ungated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix: spool-replay enforces Cursor quarantine; historical import skips quarantined sessions (incl. correlated children)

Addresses the replay half of finding #1 and finding #7 from kcap-cli PR #324
review:

- #1 (replay half): LifecycleSpoolDrain's transcript poster now parses the
  batch's vendor/session_id and drops (permanently discards) a Cursor batch
  whose session is quarantined, and treats a pending side-effect barrier as
  transient (retry later). This is the only delivery-time check the
  shutdown-spool-replay path has — a batch queued before a runtime
  rewrite-guard trip could otherwise still be replayed later.
- #7: CursorImportSource.ClassifyAsync now skips (ProbeError) any session
  whose quarantine IDENTITY is marked — resolved via the already-computed
  subagentLinks map so a correlated child is filtered under its PARENT's
  quarantine marker (CursorRewriteGuard is always keyed on the family/parent
  id for a spawned child watcher), not its own id. Previously `kcap import`
  had no awareness of the marker at all and could feed the exact corrupted
  line-number source the guard exists to shut off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r2): Cursor top-level watcher skips the below-threshold buffer

Cursor's own sessionStart hook posts (and spawns this very watcher) before any
transcript line is ever read, exactly like Antigravity's pre-spawn POST — so the
generic 10-line below-threshold buffer must not apply either. Before this fix a
top-level Cursor watcher re-added its still-unread lines to BufferedLines every
poll (the line cursor never advances while buffering) until they eventually
flushed as duplicates, and a watcher that force-quit before crossing the
artificial threshold skipped its final drain and shutdown spool, and was
permanently ineligible for the Cursor idle ceiling. Extracted into a pure,
testable SkipsThresholdBuffering predicate; child watchers are unaffected
(they never buffer regardless).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r2): bind the Cursor rewrite guard to the bytes it actually sends

Three related TOCTOU-closing fixes to the D0 rewrite guard, all stemming from the
same theme: the guard's hash must be bound to the exact bytes actually
processed/sent, and its baseline must be real from the start.

1. Bind-to-sent-bytes (finding #1): ReadNewCompleteLinesAsync now optionally
   (captureRawBytes: true, wired for vendor=="cursor") captures the raw byte
   buffer of the SAME capped read that decodes the batch's lines
   (NewTranscriptLines.SnapshotBytes). DrainNewLines's guard block now hashes
   directly from that snapshot instead of reopening the file separately to
   record the new-range/prior-zone hashes — closing the window where a rewrite
   landing between the decode read and the old reopen produced a hash for bytes
   the batch never actually came from. The post-ack checkpoint's trailing hash
   is now derived from the same already-verified snapshot instead of reopening
   the file after the RPC returns (a rewrite in flight during the ack could
   otherwise be blessed as the new baseline).

2. Reconnect rewind atomicity (finding #2): a reconnect discovering the server
   is behind the client now rewinds state.CursorByteOffset and the guard's
   checkpoint ATOMICALLY with the line cursor, via the extracted
   ApplyReconnectRewindAsync (testable without a live SignalR reconnect). The
   true byte offset of the rewound line is resolved by scanning the transcript
   (ResolveByteOffsetForLineAsync) rather than leaving the byte checkpoint at
   the later, too-far-ahead offset (which left the replayed line gap's
   new-range verification starting past the bytes it actually occupies).

3. Real full-prefix baseline (finding #3): the periodic full-prefix re-hash now
   seeds on the REAL first poll (not just lazily on the guard's own first
   VerifyFullPrefix call, which previously never happened before poll N) — a
   same-length rewrite of an already-checkpointed middle region landing in
   polls 1..N-1 now has a real baseline to be caught against at poll N, instead
   of poll N seeding the already-rewritten file as if it were the original.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r2): later nonterminal child hooks self-heal a dead watcher

Once a child's subagent-start is durably acked, every LATER NONTERMINAL Cursor
hook now also attempts to (re)spawn its child watcher, not just the child's own
sessionStart. Before this fix, only sessionStart ever called
MaybeSpawnChildWatcherAsync, so a child watcher that later exited (the
newly-enabled idle ceiling), crashed, or never actually spawned (e.g. its acked
sessionStart carried no transcript path) was never restarted. Retains the
terminal no-spawn rule for sessionEnd. EnsureWatcherRunning is idempotent
(PID+heartbeat check), so this is a cheap no-op once the watcher is alive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r2): historical import re-checks quarantine at delivery, resolves family via the live marker

1. Delivery-boundary quarantine check (finding #6): ImportSessionAsync now
   re-checks CursorMarkers.IsQuarantined FRESH, before ANY lifecycle/transcript
   delivery — previously it had no quarantine check at all, so a session
   quarantined by the live watcher's runtime rewrite guard AFTER
   ClassifyAsync (during repo probing, an interactive confirmation prompt, or
   simply queueing behind other sessions in the same import run) would still
   post session-start, every corrupted line, children, and session-end. A
   second check right before the transcript boundary catches a guard trip
   during the sessionStart POST itself: no transcript content is sent, but the
   already-server-side-created session is still best-effort closed with
   session-end so it doesn't hang open forever.

2. Family-identity fallback via the live marker (finding #7): subagentLinks is
   only ever computed from the sessions ONE import batch discovered — a
   `--session <child>` filter (or an inaccessible/omitted parent transcript)
   excludes the parent entirely, so the in-batch correlator can't produce the
   family link, and the CHILD's own (unquarantined) id was checked instead of
   its family's. ResolveQuarantineIdentity now falls back to the persisted
   CursorLiveSubagentLinker marker (written independently by the live hook
   dispatcher), resolved once at classify time and stamped onto SourceMeta as
   QuarantineIdentity so ImportSessionAsync's fresh re-check (above) uses the
   same family identity.

Regression test note: several existing tests in CursorImportSourceTests share
hardcoded session ids across ClassifyAsync/ImportSessionAsync calls, backed by
CursorMarkers' real (non-injectable) on-disk quarantine marker; ImportSessionAsync
now also reading that marker widened a latent parallel-execution race, so the
class is marked [NotInParallel] (mirrors the existing MachineIdFileTests pattern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r3): serialize reconnect rewind with drain, seed initial-resume byte offset, bound rewrite-guard capture to the new range

Three P1 follow-on findings from the round-2 guard-byte-binding rework (db65d93):

1. WatchCommand.RunWatch's Reconnected handler and main polling loop both mutated
   WatchState/CursorRewriteGuard once the hub reconnected, with no synchronization between
   them — a reconnect rewind's three writes (byte offset, checkpoint reset, line cursor)
   could interleave with a concurrently-running drain, recreating the byte/line-frontier
   divergence the r2 fix was meant to remove. Serialized both under a single
   SemaphoreSlim(1, 1) (cursorRewindGate, Cursor-only) via two new directly-testable helpers,
   GatedApplyReconnectRewindAsync/GatedDrainNewLinesAsync.

2. The INITIAL WatcherConnect registration only ever assigned state.LinesProcessed on
   resume; CursorByteOffset stayed at its default (0), so a watcher resuming at server line N
   mapped acked-line counts relative to N but their bytes from 0 — a permanent, silent
   line/byte-frontier misalignment. Extracted the reconnect-rewind's own byte-seeding logic
   into a shared SeedCursorByteOffsetAsync helper, called from both the reconnect path and
   the initial registration.

3. The Cursor watcher's captureRawBytes read materialized a buffer the size of the WHOLE
   file on every poll, including idle one-second polls with nothing new — unbounded LOH churn
   for large transcripts. ReadNewCompleteLinesAsync now accepts rawBytesReadFrom/
   newRangeByteOffset so DrainNewLines can request a BOUNDED read (the guard's own small
   trailing-tail zone plus whatever's actually new) on every poll except the rare periodic
   full-prefix cadence, which still needs — and gets — the whole file. CursorRewriteGuard's
   HashPriorZone gained an explicit snapshot-start-offset parameter so its window clips
   correctly against a non-zero-based buffer.

Regression tests: gate-composition tests proving a drain/rewind cannot observe a half-applied
counterpart while the gate is held; SeedCursorByteOffsetAsync unit + composition tests proving
a full ack of M resumed lines checkpoints at N+M, not M; bounded-capture tests proving the
buffer is sized to the new range (not file length), an idle poll allocates ~0 bytes, and a
large-file rewrite is still caught via the bounded path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r3): re-check quarantine during multi-batch import delivery

SessionImporter.SendTranscriptBatches streamed the mutable transcript file and posted one
request per 100 lines with no quarantine check at all inside the loop — the round-2 fix
only re-checked immediately before calling it. A quarantine written by the live watcher's
runtime rewrite guard AFTER the first transcript POST still let every remaining batch post.

SendTranscriptBatches now accepts an optional abortDelivery predicate, checked immediately
before every batch POST (including the first); a trip throws the new
TranscriptDeliveryAbortedException without posting the pending batch. CursorImportSource
threads its already-resolved quarantineIdentity into both the parent's own transcript send
and each subagent child's, so a quarantine trip aborts remaining batches for either — no
extra correlator work per batch. The parent's catch block reacts to the abort the same way
as the two existing quarantine boundary checks: best-effort session-end so the session
doesn't hang open "active" forever, then Failed so a re-run hits the pre-flight check and
cleanly Skips from then on.

Regression test: a 150-line transcript (two 100/50-line batches) with the quarantine marker
written the instant the first batch lands proves the second batch is never posted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r4): keep byte/line frontier in lockstep across polls, cadence, and resume

Finding #1 - a poll that consumed only blank/whitespace lines advanced
state.LinesProcessed without advancing state.CursorByteOffset (or the
rewrite guard's checkpoint) to match. The next poll's bounded decoder then
re-read the same already-"processed" blank bytes but seeded its line
numbering from the already-advanced LinesProcessed, inflating the line
count on every idle poll and mis-mapping the next real batch's ack byte
frontier. DrainNewLines' "no content, no repo change" branch now maps the
same blank-line count to its true byte offset (ByteOffsetForAckedLines,
mirroring the ack path) and re-checkpoints the guard, so a truly idle
follow-up poll decodes nothing new.

Finding #4 - CursorGuardPollCount (the periodic full-prefix cadence
counter) incremented unconditionally before the guarded file was even
opened. A transient IOException on that read still consumed the cadence
slot, so the next successful poll silently skipped its due full-prefix
baseline. The cadence decision is now peeked without mutating state, and
only committed once the guarded read actually completes without throwing.

Finding #5 - ResolveByteOffsetForLineAsync silently clamped to EOF when
the server's acknowledged resume line exceeded the local transcript's
line count (a transcript truncated/replaced while the watcher was
offline), and SeedCursorByteOffsetAsync/ApplyReconnectRewindAsync treated
that clamp as a valid baseline — seeding the rewrite guard against the
wrong (truncated) file while the line cursor advanced past it. Both
methods now return a typed "could not resolve exactly" result; the seed
path quarantines the session instead of seeding a bogus baseline, and
both the initial WatcherConnect resume and reconnect-rewind call sites
exit (cts.Cancel()) rather than proceed on an unresolved rewind.

Regression tests (CursorGuardWiringTests, CursorReconnectRewindTests): a
blank-only poll advances both frontiers together and a subsequent
genuinely-idle poll re-decodes nothing; a guarded read that throws
IOException never consumes the full-prefix cadence; a resume/reconnect
frontier beyond the local transcript's line count quarantines instead of
seeding a clamped baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r4): propagate delivery-abort quarantine through the historical import's close-and-fail path

Finding #2 - SendSubagentLifecycleAsync's catch-all swallowed the typed
TranscriptDeliveryAbortedException into a bare `false`, so a quarantine
tripped during a CHILD's own transcript delivery made ImportSessionAsync
return Failed WITHOUT ever posting the parent's best-effort session-end -
even though the child's subagent-start had already landed, leaving the
parent/subsession stuck Active forever (the quarantine marker makes the
next run Skip at preflight instead of repairing it). The child lifecycle
helper now (a) checks quarantine BEFORE posting a new child's
subagent-start, so a family found quarantined never starts another child,
and (b) rethrows TranscriptDeliveryAbortedException instead of catching
it, so the parent's child loop (now wrapped the same way the parent's own
transcript delivery already was) routes it through the shared
CloseAndFailAsync best-effort session-end + Failed contract.

Finding #3 - SendTranscriptBatches only re-checked abortDelivery BEFORE
each batch POST. A transcript that fits in a single (or final) batch -
including every transcript of <=100 lines - has no "next" batch to gate,
so a quarantine marker written while that one-and-only POST was in flight
was never observed anywhere; the method returned normally and the caller
proceeded into child lifecycle / normal completion. abortDelivery is now
also re-checked immediately AFTER every POST (mid-loop and the trailing
batch), closing that window.

Regression tests: a quarantine trip during a child's own transcript
delivery closes the parent via best-effort session-end + Failed; a later
child is never started once the family is found quarantined (even when
an earlier child's own delivery was clean); a quarantine marker appearing
during a single-batch (<=100 line) transcript's only POST is observed and
routed through close-and-fail, at both the SessionImporter.
SendTranscriptBatches level and the CursorImportSource.ImportSessionAsync
level.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r4): lockstep byte frontier in the repo-only-failed drain branch too

The "repo-only batch failed" catch branch in DrainNewLines advanced
state.LinesProcessed past blank/whitespace-only lines without advancing
state.CursorByteOffset (or the rewrite guard's checkpoint) to match -
the same byte/line-frontier drift finding #1 fixed for the "no content,
no repo change" early return, on the other path that moves the line
cursor without an acked send. This branch is reached only when a repo
change was pending (repoToSend != null) and the repo-only RPC threw with
newLines.Count == 0 (any lines read were blank); left unfixed, the next
poll's bounded decoder re-read those same blank bytes under an inflated
line number.

Extracted the finding-#1 lockstep logic into a shared local function
(AdvanceCursorBlankByteFrontierInLockstep) so both call sites - the early
return and this catch branch - map the consumed blank-line count to its
true byte offset via ByteOffsetForAckedLines (fed from the already-captured
cursorGuardSnapshot) and re-checkpoint the guard byte-for-byte identically.

Regression test: a blank-only poll with a pending repo change whose
repo-only send fails advances byte AND line frontier together, and a
subsequent idle poll (repo change still pending, still failing) re-decodes
nothing / does not inflate the line number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r5): resolve a complete unterminated final line at EOF instead of quarantining

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Review fix (r6): rewind unterminated-final-line resume so an appended terminator keeps line numbering

The r5 fix seeded CursorByteOffset at EOF while leaving LinesProcessed at
the already-acked final record's own line count. When Cursor later appended
that record's terminating newline before its next record (Cursor's normal
write order), the bounded reader started reading exactly at that EOF and
misread the leading '\n' as closing a phantom empty line — because it seeds
its own line index from LinesProcessed, already past that record. Every
following line then landed one number too high, permanently: the server,
still waiting at the true frontier, saw a persistent gap while the watcher
kept resending from the stale offset.

ResolveByteOffsetForLineAsync now returns a (ByteOffset, LineNumber) pair;
the complete-unterminated-final-record case rewinds to the record's own
start paired with LineNumber - 1 instead of EOF paired with LineNumber
unchanged, so the record is re-read/re-sent next poll — harmless, since
Cursor's normalizer emits deterministic event ids and the server's
source-ack frontier dedupes a resend at/behind it. SeedCursorByteOffsetAsync
now assigns both halves of the pair together (and, for every vendor, owns
WatchState.LinesProcessed directly rather than leaving it to two separate
call sites), keeping the byte/line frontier in lockstep from one source of
truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Qodo fix: make CursorMarkers.Quarantine fail-open on write failure

Quarantine() did unguarded filesystem writes/moves with no exception handling,
but it's called from CursorRewriteGuard.Reject deep in the watcher's drain
loop, which has no broad exception handler above it (DrainNewLines only
catches IOException/OperationCanceledException). A non-IOException failure
(e.g. UnauthorizedAccessException) would escape and crash the watcher instead
of letting the caller's Verify* return value cleanly stop delivery and exit.
Wraps the write in a try/catch, logs to stderr, and never throws — matching
IsQuarantined/ReadMarker's existing fail-open contract. Also trims the
class-level doc comment per repo comment-verbosity guidelines.

Adds a regression test that occupies the marker's own file path with a
directory (so the final rename fails) and asserts Quarantine doesn't throw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AI-1382] Qodo fix: trim over-verbose Cursor watcher comments

CursorRewriteGuard's class doc and a few method docs, plus one
CursorHookCommand block, ran to multi-paragraph design-doc-style prose.
Condenses them to the essential invariant/precondition, keeping the subtle
correctness notes (TOCTOU binding, checkpoint-reset rationale) but dropping
narrative. Also strips AI-#### Linear identifiers from code comments per repo
guideline (CLAUDE.md: "DO NOT use Linear issue numbers in comments").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

CLI profiles missing from README

1 participant