Skip to content

Recover the working directory for imported Gemini sessions - #974

Merged
George-Payne merged 3 commits into
mainfrom
georgepayne/ai-2849-gemini-import-cwd
Sep 16, 2026
Merged

George-Payne merged 3 commits into
mainfrom
georgepayne/ai-2849-gemini-import-cwd

Conversation

@George-Payne

Copy link
Copy Markdown
Member

AI-2849 — no GitHub issue (Linear-first for our own work)

What & why

Gemini names its workspace only in the <session_context> bootstrap that opens a recording: the header carries a project hash, and the per-project tmp dir is that same hash. Import therefore had no directory to place a session with, so excluded_paths and excluded_repos never matched one — and an allow list, which does not admit what it cannot place, skipped every Gemini session. Discovery now reads the bootstrap onto DiscoveredSession.Cwd, so the central capture-scope gate scopes these like every other source, and session-start forwards cwd/workspace_root as the live hook does. Antigravity stays unplaceable: nothing in its transcript records a workspace.

Where to look

GeminiSessionContext rejects far more than it accepts — a turn merely quoting the tag, a drifted heading, an empty list all read as "no workspace". A guessed directory would admit a session the allow list meant to exclude, so unplaceable is the safe answer.

--cwd rides on the same recovery, matching whole paths against the recorded workspace; the project-dir basename it can otherwise see is the tmp-dir hash.

Verification

13 new tests. Each behavioural guarantee falsified by sabotaging the code it covers:

Sabotage Tests that fail
workspace scan disabled 2
indentation guard removed 1
bootstrap-only rule removed 1
cwd dropped from session-start 1

--treenode-filter '/*/*/*Gemini*/*': 80 unit, 9 integration, 36 core — 0 failing.

@George-Payne George-Payne self-assigned this Sep 16, 2026
@linear-code

linear-code Bot commented Sep 16, 2026

Copy link
Copy Markdown

AI-2849

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Recover working directories for imported Gemini sessions

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Recover Gemini workspace paths from session bootstrap records during historical discovery.
• Apply full-path cwd filtering and central capture scope to imported sessions.
• Forward recovered cwd metadata on session start, with unit and integration coverage.
Diagram

graph TD
  A["Gemini JSONL"] --> B["Bounded Head Scan"] --> C["Context Parser"] -->|cwd or null| D["Discovered Session"] --> E["Capture Scope"] -->|admitted| F["Session Start"]
Loading
High-Level Assessment

The bounded, fail-closed bootstrap parser is the appropriate approach because the session context is Gemini's only reliable workspace source. Inferring paths from project hashes is not possible, while scanning full transcripts would add unnecessary cost and increase the risk of accepting quoted or unrelated context tags.

Files changed (6) +348 / -31

Bug fix (2) +161 / -30
GeminiSessionContext.csExtract workspace paths from Gemini bootstraps +90/-0

Extract workspace paths from Gemini bootstraps

• Adds a defensive JSONL parser for seed operations and bare user messages containing a valid opening session context. It returns only the first indented workspace entry and treats malformed, drifted, quoted, or empty contexts as unplaceable.

src/Capacitor.Cli.Core/Harness/Gemini/GeminiSessionContext.cs

GeminiImportSource.csCarry recovered Gemini cwd through import +71/-30

Carry recovered Gemini cwd through import

• Scans the first eight recording records for workspace metadata and stores it on discovered sessions. Uses normalized full paths for cwd filtering and forwards cwd plus the discovered Git root in session-start payloads.

src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs

Tests (3) +186 / -0
GeminiSessionContextTests.csCover Gemini session-context parsing rules +89/-0

Cover Gemini session-context parsing rules

• Tests seed and bare-message shapes, string and array content, first-directory selection, and conservative rejection of quoted tags, missing headings, empty lists, headers, and malformed JSON.

test/Capacitor.Cli.Core.Tests.Unit/Harness/Gemini/GeminiSessionContextTests.cs

GeminiSubagentImportTests.csVerify recovered cwd reaches session start +39/-0

Verify recovered cwd reaches session start

• Adds an integration test proving the bootstrap workspace survives discovery and classification, then appears in the Gemini session-start request.

test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs

GeminiImportSourceTests.csTest Gemini cwd discovery and filtering +58/-0

Test Gemini cwd discovery and filtering

• Verifies discovery recovers recorded workspaces, preserves null for unplaceable recordings, and filters against complete normalized workspace paths rather than project-hash directory names.

test/Capacitor.Cli.Tests.Unit/Harness/Gemini/GeminiImportSourceTests.cs

Documentation (1) +1 / -1
README.mdDocument Gemini import workspace recovery +1/-1

Document Gemini import workspace recovery

• Clarifies that only Antigravity remains inherently unplaceable. Documents that Gemini imports use the opening session context for capture scope and remain unplaceable when no workspace is recorded.

README.md

@qodo-code-review

qodo-code-review Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Forged bootstrap bypasses capture scope ✓ Resolved 🐞 Bug ⛨ Security
Description
BootstrapTexts extracts context text from every $set message and the root record without
requiring type to be user, despite only user records being documented bootstrap shapes. When an
early Gemini/model record begins with a syntactically valid block, its invented path reaches
allow/deny scope decisions and session-start repository attribution.
Code

src/Capacitor.Cli.Core/Harness/Gemini/GeminiSessionContext.cs[R44-46]

+        if (root.Obj("$set") is { } set && set.Arr("messages") is { } messages) {
+            foreach (var message in messages.EnumerateArray()) {
+                foreach (var text in TextParts(message)) yield return text;
Relevance

●● Moderate

Security concern is plausible, but no closely matching historical parser-scope precedent was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new parser searches all $set.messages entries and the root record, while TextParts checks
only the content shape and never the record type. ReadHead adopts the first workspace returned
within the head scan, CaptureScope.Stamp uses that cwd for excluded paths and allow lists, and the
import payload forwards it as cwd and derives workspace_root from it.

src/Capacitor.Cli.Core/Harness/Gemini/GeminiSessionContext.cs[29-40]
src/Capacitor.Cli.Core/Harness/Gemini/GeminiSessionContext.cs[43-63]
src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs[452-467]
src/Capacitor.Cli/Commands/CaptureScope.cs[86-116]
src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs[280-294]

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

## Issue description
Gemini workspace extraction accepts `<session_context>` content from records of any type. A Gemini/model message can consequently provide the working directory used by capture-scope checks and repository attribution.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/Gemini/GeminiSessionContext.cs[43-63]
- test/Capacitor.Cli.Core.Tests.Unit/Harness/Gemini/GeminiSessionContextTests.cs[53-62]

## Recommended Fix
Require each candidate root or `$set.messages` entry to have `type: "user"` before reading its content. Add a test where a `gemini` record begins with an otherwise valid `<session_context>` block and verify that it returns null.

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



Remediation recommended

2. A new test bypasses temp fixtures ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
ImportSession_forwards_the_workspace_recovered_from_the_bootstrap constructs paths from _tempDir
with Path.Combine, Directory.CreateDirectory, and File.WriteAllLines instead of using injected
TempDir path and creation helpers. Because the test extends a class with a manually owned _tmp
field, its setup also remains outside the framework-managed fixture lifecycle and encourages later
tests to repeat both patterns.
Code

test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[R132-134]

+        var chats = Path.Combine(_tempDir, "proj", "chats");
+        Directory.CreateDirectory(chats);
+        File.WriteAllLines(Path.Combine(chats, "session-2026-06-22T14-31-0a900002.jsonl"), new[] {
Relevance

●●● Strong

Explicit active rules require TempDir injection and helpers; historical test-fixture cleanup
findings were accepted.

PR-#172
PR-#210

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added lines directly perform path construction, directory creation, and file writing beneath
_tempDir; the class initializes that value from a manually constructed TempDir. The cited rules
require temporary paths to use TempDir helpers and require test classes to receive TempDir
through framework injection rather than owning fields.

Rule 2767472: Use TempDir helper for all temporary filesystem state in tests
Rule 2767480: Dispose TempDir fields outside test classes and prefer TempDir injection inside test classes
Rule 2808173: Use injected [TempDir] public required property in test classes instead of manual fields
test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[20-27]
test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[132-134]

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 added integration test directly creates paths and files under a manually managed temporary directory instead of using the injected `TempDir` fixture and its helper methods.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[20-27]
- test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[132-134]

## Recommended Fix
Replace the manually owned `_tmp` and derived `_tempDir` fields with a public required `[TempDir]` property. Use its `CreateDir`, `CreateFile`, and `PathTo` members throughout the class, including the newly added test.

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


3. A test heading repeats its method ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The // --- The working directory recovered from the <session_context> bootstrap --- comment is
only a section label that repeats the immediately following test name. It records no constraint,
rationale, or behavioral trap, so a later reader learns nothing that the method declaration does not
already communicate.
Code

test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[125]

+    // --- The working directory recovered from the <session_context> bootstrap ---
Relevance

●●● Strong

Recent history accepts removing decorative or redundant test comments.

PR-#480
PR-#580

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed line is a decorative heading whose subject is already expressed by the following test
method. Compliance rule 2762993 permits comments for non-obvious, behavior-critical constraints
rather than comments that merely restate nearby code.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[125-128]

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 added section-heading comment merely restates the test immediately below it and does not document a non-obvious behavioral constraint.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs[125-125]

## Recommended Fix
Remove the decorative heading comment. Keep the descriptive test method name as the sole explanation of this test section.

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


4. Imported sessions lose repository roots ✗ Dismissed 🔗 Cross-repo conflict ≡ Correctness
Description
BuildSessionStartPayload sends workspace_root, but the server’s Gemini hook contract has no
matching field and persists only cwd. Whenever the recovered directory is inside a Git repository,
workspace-root consumers such as session metadata and collision tracking receive no root.
Code

src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs[R288-290]

+        if (cwd is not null) {
+            payload["cwd"] = cwd;
+            if (GitRepository.FindRoot(cwd) is { } workspaceRoot) payload["workspace_root"] = workspaceRoot;
Relevance

●● Moderate

Contract mismatch could affect repository attribution, but no repository-root payload precedent was
found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR emits workspace_root, while the pinned server’s Gemini DTO does not declare it and its
handler copies only cwd into an extension with no workspace-root member. The generic
workspace-root accessor consequently cannot expose Gemini roots, and collision tracking deliberately
depends on that accessor without falling back to cwd.

src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs[280-294]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public.Abstractions/Hooks/GeminiSessionStartHook.cs [5-19]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public/Hooks/GeminiHookHandlers.cs [71-84]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Core/GeminiExtensions.cs [66-78]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Core/SessionStartedAccessors.cs [63-72]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Collisions/CollisionsProjector.cs [244-261]

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

## Issue description
Imported Gemini session-start requests send `workspace_root`, but kcap-server's Gemini request DTO and extension do not accept or persist it, so downstream workspace-root consumers cannot use it.

## Fix Focus Areas
- src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs[288-290]
- /cross_repos/kcap-server/src/Capacitor.Api.Public.Abstractions/Hooks/GeminiSessionStartHook.cs[5-19]
- /cross_repos/kcap-server/src/Capacitor.Api.Public/Hooks/GeminiHookHandlers.cs[71-84]
- /cross_repos/kcap-server/src/Capacitor.Server.Core/GeminiExtensions.cs[66-78]
- /cross_repos/kcap-server/src/Capacitor.Server.Core/SessionStartedAccessors.cs[63-72]

## Recommended Fix
Coordinate a kcap-server change that adds a JSON-mapped `workspace_root` property to `GeminiSessionStartHook`, stores it in `GeminiSessionExtension`, and includes Gemini in `SessionStarted.WorkspaceRoot()`. Add a server round-trip test, then release or deploy that server support with the CLI change.

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


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: 4e78490c)
Review mode: ⚖️ Balanced: This changes Gemini import discovery, workspace parsing, capture-scope filtering, and session-start payload behavior across production and integration paths, creating meaningful correctness and compatibility risk but not enough independent logic density to justify extended review.

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs
Comment thread test/Capacitor.Cli.Tests.Integration/GeminiSubagentImportTests.cs Outdated
Comment thread src/Capacitor.Cli.Core/Harness/Gemini/GeminiSessionContext.cs
Comment thread src/Capacitor.Cli/Harness/Gemini/GeminiImportSource.cs
Base automatically changed from georgepayne/ai-2845-allowed-paths to main September 16, 2026 14:30
Gemini names its workspace only in the <session_context> bootstrap that opens a
recording — the header carries a project hash, and the per-project tmp dir is
that same hash — so import had no directory to place a session with. Capture
scope could not match excluded_paths or excluded_repos against one, and an allow
list, which does not admit what it cannot place, skipped every Gemini session.

Discovery now reads the bootstrap and carries the directory onto
DiscoveredSession.Cwd, so the one central gate that scopes every other source
scopes these too, and session-start forwards cwd and workspace_root the way the
live hook does. A recording that names no workspace stays unplaceable.

--cwd now compares whole paths against the recorded workspace rather than the
project dir's basename, which is the tmp-dir hash and matched nothing.

Antigravity stays unplaceable: nothing in its transcript records a workspace.
The model writes into the same transcript the workspace is read from, so a turn
that opens with a syntactically valid <session_context> block would hand capture
scope an invented directory — enough to place a session the profile's lists meant
to exclude. Only a user record is ever a bootstrap, which is the rule the server
normalizer already applies to the same block.
var scanned = 0;

try {
foreach (var line in File.ReadLines(path)) {

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.

ReadHead now keeps File.ReadLines open while it scans and parses up to eight records of a Gemini-owned transcript. File.ReadLines opens via StreamReader with FileShare.Read, which denies a concurrent writer on Windows; Gemini appending during import can hit a sharing violation (or this read can fail), even though the later ReadTranscriptStatsAsync path correctly uses FileShare.ReadWrite. Please open this head scan with a FileStream using FileShare.ReadWrite and a StreamReader too. The old one-line header read had the same underlying risk, but this change lengthens that write-denying window while doing workspace extraction.

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.

Confirmed and fixed in 820443b.

Switched to File.ReadLinesShared, Core's streamed shared read (FileShare.ReadWrite | FileShare.Delete), which is what the other transcript scans already use — SecondaryRepoRoots, WatchCommand, and ImportCommand's evidence read. Its own doc comment names this exact failure: File.ReadLines opens FileShare.Read, which "on Windows is mandatory sharing and locks a coding agent out of the file it owns".

Your point about the lengthened window is the part that made it worth fixing rather than noting. The previous one-line header read opened and closed inside a single MoveNext; the head scan holds the enumerator across up to eight records while JSON-parsing each one, so the window now spans the workspace extraction rather than a single read.

Gemini suites after the change: Core 37, Unit 80, Integration 9, all passing.

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.

Verified on 820443b: ReadHead now uses File.ReadLinesShared, whose stream opens with FileShare.ReadWrite | FileShare.Delete. This resolves the Windows sharing finding. I am approving this head.

@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.

One actionable Windows file-sharing issue in the new transcript head scan; details are in the inline comment. Static review only; I did not build or run tests.

The head scan holds the file open across up to eight records while parsing
each, and File.ReadLines opens FileShare.Read — mandatory sharing on Windows,
so a Gemini process appending to the transcript it owns is locked out for that
window.

File.ReadLinesShared is the streamed shared read the rest of the transcript
scans already use.
@realtonyyoung

Copy link
Copy Markdown
Collaborator

NO FINDINGS on 820443b. Re-reviewed the Gemini workspace extraction and verified the shared-read fix resolves my prior Windows file-sharing finding. Static review only; I did not build or run tests.

@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.

Approved after static re-review; the prior finding is resolved.

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.

2 participants