Skip to content

[AI-2176] CLI: evidence-based repo association for sessions started outside a repo - #648

Merged
realtonyyoung merged 8 commits into
mainfrom
claude-tyoung/ai-2176-repo-evidence
Aug 22, 2026
Merged

realtonyyoung merged 8 commits into
mainfrom
claude-tyoung/ai-2176-repo-evidence

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Part of AI-2176.

What & why

Coding-agent sessions launched outside any git repo (e.g. from a parent dir like ~/dev that holds sibling checkouts) currently never get associated with the repo they actually work in — repo identity is derived once from the launch cwd, and kcap watch re-probes that same cwd forever. This is the live-detection half of the fix: when the launch cwd is not in a git tree, derive the repo once from evidence — the absolute file paths in the session's Claude tool-use inputs — and deliver it over the existing per-batch repository payload, so the server's existing first-wins freeze fills the NULL primary. No server change is needed for this path.

(The companion server PR adds the admin backfill for already-recorded sessions. Full design: spec docs/superpowers/specs/2026-08-21-ai2176-repo-association-evidence-design.md, rev 7, in the kcap-server repo.)

Changes

  • RepoEvidenceScanner<TRepo> (Capacitor.Cli.Core/RepoEvidence/) — pure, generic scanner. From each Claude tool_use input it extracts absolute paths (mutation tools Edit/MultiEdit/Write/NotebookEdit first, read tools Read/Glob/Grep as fallback), resolves each candidate root via injected findRoot + async resolve, and attributes the first mutation-derived root that resolves to a detectable owner/repo (read-derived root is the fallback, promoted only if no mutation attributed). Resolution is part of the win condition, so a no-remote/unresolvable root never latches; per-root resolve caching bounds the git cost. Path extraction lives in the non-generic RepoEvidencePaths.
  • Watcher (WatchCommand.cs) — creates the scanner only for a Claude session watcher whose launch cwd is not in a repo (in-repo sessions are completely unaffected — D1). One-shot prefix scan on startup (restart/crash recovery), per-drained-line feeding, and final-drain promotion of the read fallback before the batch is sent over the live hub (so read-only sessions deliver on a clean session end, not only abnormal exits). The 60s launch-cwd refresh can never clear an evidence-derived payload.
  • Import (ImportCommand.cs) — kcap import applies the same scanner to newly-imported transcripts whose launch cwd is not in a repo, attaching a repository node to the session-start payload (fail-open on an unreadable transcript).

Tests

  • RepoEvidenceScannerTests (10) — two-slot rule, mutation-over-read priority, incomplete-root-doesn't-latch-then-later-complete-root-wins, stop-after-attribution, relative/temp/non-repo paths ignored, fail-open, evidence-is-input (a failed tool result still counts), tool classification.
  • WatchRepoEvidenceTests (3) — the 60s-refresh guard never clears an evidence payload; final-drain delivers the read fallback to state.Repository; non-final drain does not promote.
  • ImportRepoEvidenceTests (5) — outside-repo transcript gains a repository node; read-only promotes the fallback; no evidence → null; invalid path degrades to null.

Scope notes

  • Unix-absolute paths only in the live scanner (per the design). Windows Claude sessions started outside a repo get no live attribution but are covered by the server-side backfill (which handles C:\/C://UNC forms).
  • Vendor scope: Claude Code watcher + import in v1 (Cursor and other vendors are follow-ups).
  • Very short (<10 transcript line) sessions are dropped by the existing watcher threshold before evidence scanning — a conscious limitation; the server-side backfill re-homes any residual repo_hash IS NULL rows.

🤖 Generated with Claude Code

…ack via final-drain batch

Spec review findings on the watcher-integration work:

- Scanner latched Done the moment FindRoot found a .git dir, before checking
  whether the root resolved to a usable owner/repo. A no-remote local repo
  edited first permanently blocked a later real GitHub repo from attributing.
  RepoEvidenceScanner is now generic over TRepo with an injected async
  resolver + completeness predicate; a root is only latched once resolved
  AND complete, and each distinct root is resolved at most once (cached).

- The read-fallback promotion for a clean session end (StopWatcher, no
  parent-exit) was applied after the final drain's batch had already been
  sent, so a correctly-computed fallback was never transmitted. Promotion
  now happens inside DrainNewLines's isFinalDrain branch, before repoToSend
  is computed, so it rides the same live-hub batch that goes out on every
  exit that runs a final drain.
Part A whole-branch review, item 1: the final-drain promote-and-deliver
path (Finding 1's fix) was verified only by inspection. Extract it into
WatchCommand.ApplyEvidenceScanAsync — a small seam over WatchState + the
generic scanner, no HubConnection needed — and unit-test with a fake
scanner that a read-only line's fallback lands on state.Repository when
isFinalDrain is true, and stays untouched when it's false.
…ce scan

Part A whole-branch review, item 2: File.ReadLines(session.FilePath) was
passed as a call argument to TryBuildEvidenceRepositoryNodeAsync, i.e.
evaluated outside that helper's own try/catch. File.ReadLines validates
its path argument eagerly (confirmed: an empty path throws ArgumentException
synchronously, before the lazy file read), so an invalid path would mark an
otherwise-importable session Errored instead of importing without evidence.

Add a path-based overload that reads inside its own try and delegates to
the existing line-based one; the call site now passes session.FilePath
directly instead of pre-evaluating File.ReadLines.
@linear-code

linear-code Bot commented Aug 22, 2026

Copy link
Copy Markdown

AI-2176

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

CLI: infer repository from tool-use path evidence for outside-repo sessions

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Infer repo for Claude sessions started outside a git checkout using tool-use path evidence.
• Prefer mutation tool paths over read paths; promote read fallback on clean session end.
• Extend both kcap watch and kcap import, with fail-open behavior and unit coverage.
Diagram

graph TD
  WC["WatchCommand"] --> SC(["RepoEvidenceScanner"])
  IC["ImportCommand"] --> SC(["RepoEvidenceScanner"])
  TR[("Transcript JSONL")] --> WC["WatchCommand"] --> HUB[["Live hub batch"]]
  TR[("Transcript JSONL")] --> IC["ImportCommand"]
  SC(["RepoEvidenceScanner"]) --> GR["FindRoot + Resolve"]
  GR["FindRoot + Resolve"] --> SC(["RepoEvidenceScanner"])
  
  subgraph Legend
    direction LR
    _cmd["Command"] ~~~ _scan(["Scanner"]) ~~~ _data[("Data") ] ~~~ _ext[["External/API"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Server-side evidence inference only
  • ➕ Centralizes inference and avoids extra client-side git/gh detection cost
  • ➕ Can backfill uniformly for all ingestion paths
  • ➖ Requires server changes and deployment coordination
  • ➖ Still needs a client-to-server way to transmit evidence (paths) or full transcripts
2. Continuous re-attribution scanning during watch
  • ➕ Could eventually attribute even if early evidence was incomplete or non-repo
  • ➕ Better for long sessions where repo context changes
  • ➖ Risks churn/flip-flop without careful ‘first-wins’/stability rules
  • ➖ Adds more complexity to batching and state management

Recommendation: Current approach is the best fit for the stated constraints (no server change, first-wins freeze). The scanner’s completeness-gated resolution and cached root resolving avoids the earlier ‘no-remote repo latches forever’ failure mode while keeping watch/import changes localized and testable.

Files changed (7) +533 / -1

Enhancement (4) +279 / -1
Models.csTrack evidence-based repo state in WatchState +8/-0

Track evidence-based repo state in WatchState

• Adds 'EvidenceScanner' and 'RepositoryFromEvidence' to 'WatchState' to support repo attribution when the watcher starts outside a git checkout. The flag prevents later cwd-based refresh probes from clearing an evidence-derived repository back to null.

src/Capacitor.Cli.Core/Models.cs

RepoEvidenceScanner.csAdd generic RepoEvidenceScanner and path extraction for Claude tool_use inputs +123/-0

Add generic RepoEvidenceScanner and path extraction for Claude tool_use inputs

• Introduces a generic, fail-open scanner that extracts absolute paths from Claude tool-use *inputs*, maps them to candidate git roots, resolves roots asynchronously, and only latches when the resolved repo is complete. Implements a two-slot rule: mutation evidence attributes immediately; read evidence is stored as a fallback and promoted only on final drain.

src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs

ImportCommand.csInfer repository during import when launch cwd is outside any repo +70/-0

Infer repository during import when launch cwd is outside any repo

• Adds transcript-wide evidence scanning to attach a 'repository' node to the session-start hook when cwd-based detection fails. Provides both line-enumerable and path-based overloads with defensive try/catch to ensure unreadable/invalid transcript paths fail open rather than erroring the import.

src/Capacitor.Cli/Commands/ImportCommand.cs

WatchCommand.csEvidence-based repo attribution for outside-repo Claude sessions, with final-drain fallback delivery +78/-1

Evidence-based repo attribution for outside-repo Claude sessions, with final-drain fallback delivery

• Creates and seeds an evidence scanner only for Claude session watchers launched outside a git repo, including a best-effort prefix scan for restart/crash recovery. Integrates per-drain scanning and promotes read fallback on the final drain so the repository payload rides the same last batch; also prevents periodic cwd refresh from nulling an evidence-derived repo.

src/Capacitor.Cli/Commands/WatchCommand.cs

Tests (3) +254 / -0
RepoEvidenceScannerTests.csUnit tests for evidence scanning rules and tool classification +131/-0

Unit tests for evidence scanning rules and tool classification

• Adds coverage for mutation-over-read priority, read fallback promotion, stop-after-attribution behavior, incomplete-root non-latching, ignoring non-absolute paths, vendor gating, and fail-open parsing. Verifies Claude v1 tool classification for mutation and read evidence.

test/Capacitor.Cli.Core.Tests.Unit/RepoEvidenceScannerTests.cs

ImportRepoEvidenceTests.csUnit tests for import-time evidence repository node creation +72/-0

Unit tests for import-time evidence repository node creation

• Validates that outside-repo transcripts gain a repository node, that read-only transcripts promote the read fallback, and that no-evidence yields null. Includes a regression guard ensuring invalid/empty transcript paths fail open rather than throwing due to eager 'File.ReadLines' argument validation.

test/Capacitor.Cli.Tests.Unit/ImportRepoEvidenceTests.cs

WatchRepoEvidenceTests.csUnit tests for watch-time evidence behavior and final-drain fallback delivery +51/-0

Unit tests for watch-time evidence behavior and final-drain fallback delivery

• Ensures repository refresh logic never clears an evidence-derived payload. Adds regression tests asserting read-only evidence is only promoted on the final drain (delivery path), not during non-final drains.

test/Capacitor.Cli.Tests.Unit/WatchRepoEvidenceTests.cs

@qodo-code-review

qodo-code-review Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Windows paths never scanned ✓ Resolved 🐞 Bug ≡ Correctness
Description
RepoEvidencePaths.ExtractClaudePaths only accepts paths starting with '/', so Windows-absolute tool
paths (e.g. C:\repo\file.cs or C:/repo/file.cs) are discarded and the evidence scanner cannot
ever attribute a repository on Windows.
Code

src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs[R114-116]

+                var path = input[sp.key]?.GetValue<string>();
+                if (path is not null && path.StartsWith('/')) result.Add((path, sp.kind));
+            }
Evidence
The new code explicitly filters to paths beginning with '/', while existing code demonstrates
Windows-specific path handling (case-insensitive matching and both separators). This mismatch means
Windows absolute tool paths will be ignored by the scanner.

src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs[114-116]
src/Capacitor.Cli/CwdRemapper.cs[13-21]

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

### Issue description
`ExtractClaudePaths` filters candidate paths with `path.StartsWith('/')`, which only recognizes POSIX absolute paths. On Windows this drops the vast majority of absolute file paths, so the evidence scanner won’t detect any repos.

### Issue Context
The codebase explicitly supports Windows path semantics elsewhere (case-insensitive comparisons, both separators), so this is a functional regression for Windows users.

### Fix Focus Areas
- src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs[114-116]

### Proposed fix
Replace the `StartsWith('/')` gate with a robust “absolute path” predicate:
- Prefer `Path.IsPathFullyQualified(path)` (works on Windows and Unix).
- Optionally keep accepting leading `/` explicitly if needed for transcript portability.
Maintain the fail-open behavior if `Path.*` throws (wrap in try/catch or keep within existing try).

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


2. Misses root.content blocks ✓ Resolved 🐞 Bug ≡ Correctness
Description
RepoEvidencePaths.ExtractClaudePaths only reads obj["message"]["content"] and will ignore valid
Claude assistant tool-use blocks stored at the top-level content array, causing evidence scanning
to miss all paths and never attribute a repository for those transcripts.
Code

src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs[R93-96]

+            if (JsonNode.Parse(jsonlLine) is not JsonObject obj) return result;
+            if (obj["type"]?.GetValue<string>() != "assistant") return result;
+            if (obj["message"]?["content"] is not JsonArray content) return result;
+
Evidence
The new extractor only looks for message.content, but existing import parsing explicitly supports
both root.message.content and root.content for assistant events. Therefore, the new scanner will
miss tool_use blocks for transcripts using the root.content layout.

src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs[93-96]
src/Capacitor.Cli/Commands/SessionImporter.cs[337-341]

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

### Issue description
`RepoEvidencePaths.ExtractClaudePaths` assumes Claude JSONL lines always store tool blocks under `message.content`. Elsewhere in the codebase, Claude “assistant” events are documented to appear either under `root.message.content` or `root.content`. When the transcript uses the latter, evidence scanning will produce no paths and repo attribution will never happen.

### Issue Context
This breaks the main feature (evidence-based repo association) for transcripts in the `root.content` shape.

### Fix Focus Areas
- src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs[93-96]

### Proposed fix
Update extraction to accept both layouts, e.g.:
- `var content = (obj["message"]?["content"] as JsonArray) ?? (obj["content"] as JsonArray);`
- iterate that `content` when present.
Keep the current fail-open behavior.

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


3. WatchCommand uses File.ReadLines ✓ Resolved 📘 Rule violation ☼ Reliability
Description
New transcript-reading code paths in both WatchCommand and the evidence-based import overload scan
the agent-owned transcript via File.ReadLines, which can deny write access on Windows and break
agent flush/shutdown. Agent-owned transcript reads must use shared-read helpers or a FileStream
opened with FileShare.ReadWrite.
Code

src/Capacitor.Cli/Commands/WatchCommand.cs[500]

+                foreach (var line in File.ReadLines(transcriptPath)) {
Evidence
PR Compliance ID 26 forbids reading agent-owned transcript files with write-denying share modes and
instead requires shared-read semantics (via shared-read helpers or opening a FileStream with
FileShare.ReadWrite). The cited changes introduce transcript prefix/evidence scanning that calls
File.ReadLines(transcriptPath), which typically opens the file in a way that can prevent the agent
from writing, demonstrating noncompliance with the required sharing behavior.

CLAUDE.md: Never Read Agent-Owned Files With Write-Denying Share Modes; Use Shared-Read Helpers
src/Capacitor.Cli/Commands/WatchCommand.cs[490-506]
src/Capacitor.Cli/Commands/ImportCommand.cs[2211-2226]

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

## Issue description
`WatchCommand` and the path overload in `ImportCommand.TryBuildEvidenceRepositoryNodeAsync` read the agent-owned transcript using `File.ReadLines(transcriptPath)`, which typically opens the file with write-denying sharing on Windows and can block the agent’s own writes during flush/shutdown.

## Issue Context
Compliance (PR Compliance ID 26) requires that reads of agent-written transcripts/sidecars use shared-read semantics so the agent can continue writing while the CLI scans/loads transcript content; this should be implemented via existing shared-read helpers or by opening a `FileStream` with `FileShare.ReadWrite` and reading lines from that stream.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/WatchCommand.cs[494-506]
- src/Capacitor.Cli/Commands/ImportCommand.cs[2217-2225]

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


Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli/Commands/WatchCommand.cs Outdated
Comment thread src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs
Comment thread src/Capacitor.Cli.Core/RepoEvidence/RepoEvidenceScanner.cs
… too

Windows CI + qodo review on PR #648:

- qodo #3 / Windows CI failure: SafeDirectory used Path.GetDirectoryName,
  which rewrites/misreads the OTHER OS's separator style (a Unix path run
  through Windows' Path becomes garbage, and vice versa) instead of the
  transcript-producing OS's own convention. Replaced with a lexical
  last-separator split. ExtractClaudePaths' path.StartsWith('/') gate
  likewise dropped every Windows-absolute path; replaced with
  IsLexicallyAbsolute, mirroring RepoAttributionMatcher.IsLexicallyAbsolute
  from the server repo (reimplemented locally, no dependency taken) —
  Unix-rooted, Windows drive-rooted, or UNC, purely lexical either way.

- qodo #2: a tool_use block can sit at an assistant event's top-level
  content, not only nested under message.content; ExtractClaudePaths now
  checks both.
qodo #1 (High) on PR #648: both new transcript reads (the watcher's
one-shot prefix scan, and ImportCommand's path-based evidence overload)
used File.ReadLines, which opens FileShare.Read — mandatory-exclusive on
Windows, so it can deny the write handle the agent itself still holds on
that same transcript mid-flush.

Added WatchCommand.ReadLinesShared: a line-yielding sibling of the
existing ReadAllTextShared/ReadAllTextSharedAsync helpers (same
FileShare.ReadWrite, same rationale, doc-commented in place already),
streamed rather than materialized so a scan that attributes early doesn't
pay for the whole file. Both call sites now use it; ImportCommand reuses
it directly since it's in the same project/namespace.
@realtonyyoung
realtonyyoung merged commit 6d176c3 into main Aug 22, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the claude-tyoung/ai-2176-repo-evidence branch August 22, 2026 13:30
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.

1 participant