Skip to content

Snapshot submodule contents as plain files instead of refusing the repo - #489

Merged
realtonyyoung merged 3 commits into
mainfrom
tonyyoung/ai-1794-snapshot-submodules
Aug 7, 2026
Merged

realtonyyoung merged 3 commits into
mainfrom
tonyyoung/ai-1794-snapshot-submodules

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Fixes AI-1794

The problem

The IndependentSnapshot borrowed-review lane refused any repository whose index carried a gitlink:

if (SplitNulRecords(initialIndex).Any(entry => entry.Span.StartsWith("160000 "u8)))
    throw new InvalidOperationException("borrowed_snapshot_submodules_unsupported");

kcap-server pins src/cli as a submodule, so our own primary repo could not be borrowed-reviewed by any snapshot vendor. Measured effect across vendors on a kcap-server changeset:

vendor containment outcome
codex borrows in place ✅ sees the real tree
cursor IndependentSnapshot ❌ this guard
copilot IndependentSnapshot ❌ this guard
claude none ⚠️ falls back to an owned worktree — a fresh clone, so the submodule is at the stale pin. It does not error; it reviews code the branch never wrote and returns clean.

Approach

Submodules are snapshotted as plain content. Borrowing means the reviewer sees the developer's working tree, and for a submodule that is the checked-out files — not the pinned commit a fresh clone gives. Dirty and untracked files inside a submodule are carried for the same reason they are in the superproject.

Its .git is never copied: that is a gitlink file pointing into the superproject's .git/modules, which the snapshot deliberately does not have, so copying it would leave a dangling reference. The submodule gains no git identity inside the snapshot.

Security-relevant details

This lane has history (AI-1675 was a credential-exfiltration-via-symlinks finding), so the new path source deliberately gets no shortcuts:

  • Every submodule path is prefixed to be relative to the SUPERPROJECT before it enters the manifest loop, so it flows through the identical containment, symlink, reserved-path, encoding and capacity guards. A submodule is not a second, weaker lane. Notably EnsureNoLinkedComponents now walks the submodule directory component too, so a symlinked submodule dir is caught.
  • Prefixes stay raw bytes until that loop's own decode, preserving its classify-before-decode guarantee — a non-UTF8 submodule path can't slip past ClassifyReservedPath.
  • The submodule directory is resolved through ContainedPath before its git is invoked, so an index-supplied path cannot steer git -C outside the source tree.
  • An uninitialized submodule (nothing checked out) is skipped rather than failing the snapshot.

Testing

BorrowedSnapshot_CarriesSubmoduleFilesAsPlainContentWithoutItsGit — watched failing first with exactly borrowed_snapshot_submodules_unsupported. It asserts the submodule's dirty tracked file and its untracked file both arrive (so it can't pass on a pinned-commit checkout), and that no .git exists at the submodule path in either file or directory form, while the superproject's own independent .git still does.

Worth noting: the guard being removed had no test at all — rg submodules_unsupported test/ returned nothing.

Local: WorktreeManagerTests 22 total / 0 failed, BorrowedReviewContextTests 19/0, AgentOrchestratorVendorTests 207/0.

Out of scope

Option 4 from the issue — making the claude owned-worktree fallback refuse or warn on a submodule repo instead of silently reviewing at the stale pin — is deliberately not in this PR. It's a different mechanism (no snapshot involved) and separable, but it is the more dangerous half of the original report, since it fails silently rather than loudly. Worth its own issue.

🤖 Generated with Claude Code

The independent-snapshot lane threw borrowed_snapshot_submodules_unsupported on
any index carrying a gitlink, so borrowed review was impossible on every repo
using a submodule — including kcap-server, which pins its CLI that way and so
could not be reviewed by any snapshot vendor at all.

Submodules now arrive as PLAIN CONTENT. Borrowing means the reviewer sees the
developer's working tree, and for a submodule that is the checked-out files —
not the pinned commit a fresh clone would give. Dirty and untracked files in a
submodule are carried for exactly the same reason they are in the superproject.

Their .git is never copied: it is a gitlink file pointing into the
superproject's .git/modules, which this snapshot deliberately does not have, so
carrying it would leave a dangling reference. The submodule gains no git
identity inside the snapshot.

Every submodule path is prefixed to be relative to the SUPERPROJECT before it
enters the manifest loop, so it flows through the same containment, symlink,
reserved-path, encoding and capacity guards as a superproject path — a
submodule is not a second, weaker lane. Prefixes stay raw bytes until that
loop's own decode, preserving its classify-before-decode guarantee, and the
submodule directory is resolved through ContainedPath before its git is
invoked, so an index-supplied path cannot steer `git -C` out of the tree.

An uninitialized submodule (nothing checked out) is skipped rather than failing
the snapshot.
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

AI-1794

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Snapshot submodule working-tree files in borrowed snapshots (no submodule .git)

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Allow borrowed snapshots to include git submodules as plain working-tree files
• Prefix submodule paths into the superproject manifest to reuse existing security guards
• Add a regression test covering dirty/untracked submodule content and .git omission
Diagram

graph TD
  A["Create borrowed snapshot"] --> B["Read index (ls-files --stage)"] --> C["Read submodule prefixes"] --> D["List source files (ls-files -co)"] --> E{"Submodule checked out?"}
  E -- "yes" --> F["git -C submodule ls-files (prefixed)"] --> G["Build unified manifest"] --> H["Copy + prune snapshot"]
  E -- "no" --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Clone/update submodules in the snapshot repo (pinned commits)
  • ➕ Deterministic content matches the superproject’s pinned submodule SHAs
  • ➕ Avoids invoking git in multiple working directories during manifest building
  • ➖ Breaks the meaning of "borrowed" review (dirty/untracked submodule changes wouldn’t be visible)
  • ➖ Requires network/transport and submodule init/update logic; slower and more failure-prone
  • ➖ Expands security surface (additional fetches, hooks/config interactions)
2. Treat submodules as excluded paths
  • ➕ Simpler implementation; avoids submodule-specific git calls
  • ➖ Reviewer misses submodule code entirely (often a large part of the repo)
  • ➖ Still leaves the repository non-reviewable for vendors that rely on the snapshot being complete

Recommendation: The chosen approach (snapshot submodules as plain working-tree content and never copy the submodule .git) is the best fit for borrowed review: it preserves what the developer actually has checked out, avoids dangling gitlinks in the snapshot, and—critically—routes submodule paths through the exact same containment/reserved-path/symlink/capacity pipeline by prefixing them into the superproject manifest. Alternatives either change semantics (pinned-only) or omit important code (exclude).

Files changed (2) +126 / -15

Bug fix (1) +76 / -15
WorktreeManager.csInclude checked-out submodule files in borrowed snapshot manifest +76/-15

Include checked-out submodule files in borrowed snapshot manifest

• Removes the hard failure on gitlink entries and instead extracts submodule path prefixes from the index. Builds the snapshot manifest by combining superproject 'ls-files' output with per-submodule 'ls-files' output, prefixing submodule paths back into the superproject so existing reserved-path, encoding, containment, symlink, and capacity guards apply uniformly. Skips uninitialized submodules and avoids copying submodule '.git' by only snapshotting file content.

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs

Tests (1) +50 / -0
WorktreeManagerTests.csAdd regression test for submodule content snapshotting +50/-0

Add regression test for submodule content snapshotting

• Adds a unit test that creates a superproject with a submodule, modifies tracked content and adds an untracked file inside the submodule, then asserts the borrowed snapshot contains those files. Also verifies the snapshot does not include a '.git' file/directory inside the submodule while still having an independent '.git' at the snapshot root.

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

Review found a real escape. ContainedPath is LEXICAL — Path.GetFullPath does
not resolve links — so a gitlink whose working-tree directory is a symlink
passed containment while `git -C` followed it and enumerated a tree outside the
source. The per-file symlink guards cannot undo a directory-level traversal
that already happened.

The new test proves the traversal rather than merely asserting a message: before
the fix it failed with "git ls-files ... fatal: not a git repository", i.e. git
had already run inside the symlink target. Had that directory been a repo, its
files would have been snapshotted.

Also from review:
- the prefix decode now raises the coded borrowed_snapshot_invalid_path_encoding
  instead of a bare DecoderFallbackException
- nested submodules FAIL with a coded error rather than being silently omitted;
  a silently incomplete snapshot is invisible to the reviewer, who would then
  report on source it never saw
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Copilot review flow: clean at round 2 (context-only). Round 1 found a real security escape — the best finding of the run — plus two correctness gaps. All three fixed in c9e8... (see latest commit).

The escape (reviewer's #1/#6)

ContainedPath is lexical — Path.GetFullPath does not resolve symlinks — so a gitlink whose working-tree directory is a symlink passed containment while git -C followed it and enumerated a tree outside the source. The per-file symlink guards can't undo a directory-level traversal that already happened.

The test was written first, and its RED output proves the traversal rather than merely asserting a message:

git ls-files -co --exclude-standard -z failed: fatal: not a git repository (or any of the parent directories): .git

git had already been invoked inside the symlink target. Had that directory been a git repo, its files would have been enumerated and copied into the snapshot.

Fix: EnsureNoLinkedComponents (the existing per-component reparse-point walk) now runs on the submodule directory before git is invoked there.

Also fixed

  • Non-UTF-8 submodule path now raises the coded borrowed_snapshot_invalid_path_encoding instead of a bare DecoderFallbackException.
  • Nested submodules now fail with borrowed_snapshot_nested_submodules_unsupported rather than being silently omitted. The reviewer's argument was the stronger one: silent incompleteness is invisible to a reviewer, who would then report on source it never saw.

Deliberately unchanged

An uninitialized submodule still skips silently. The asymmetry with the nested case is intentional: an uninitialized submodule has no checked-out content in the developer's own tree either, so the snapshot faithfully mirrors what they have — whereas nested content exists and would be dropped. Reviewer agreed on round 2.

WorktreeManagerTests 23 total / 0 failed.

@qodo-code-review

qodo-code-review Bot commented Aug 7, 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. Uninitialized submodule causes failure ✓ Resolved 🐞 Bug ☼ Reliability
Description
ReadSourceManifestAsync treats a submodule as initialized based only on Directory.Exists(subDir) and
then runs git -C subDir ls-files with clean-exit required; an existing but uninitialized/non-repo
submodule directory will throw and abort snapshot creation. This contradicts the intended “skip
uninitialized submodule” behavior.
Code

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[R931-934]

+            var subDir = ContainedPath(source, NormalizeRelativePath(StrictUtf8.GetString(prefix)));
+            if (!Directory.Exists(subDir)) continue;   // uninitialized submodule: nothing checked out
+
+            stdoutRecords.AddRange(await RunLsFilesPrefixedAsync(subDir, prefix, trackedArgs, ct));
Evidence
The new code invokes git -C subDir for every index-derived submodule prefix whenever the directory
exists, and RunLsFilesPrefixedAsync requires a clean git exit, so a non-repo directory will abort
the snapshot.

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[898-906]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[919-936]

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

### Issue description
`ReadSourceManifestAsync` only checks `Directory.Exists(subDir)` before running `git -C subDir ls-files ...`. For an uninitialized submodule, the directory can exist while not being a usable git working tree, causing `RunGitCaptureBytes(... requireCleanExit: true ...)` to throw and fail the whole snapshot.

### Issue Context
The PR intends to *skip* uninitialized submodules. The current check only covers the “directory missing” case.

### Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[898-917]
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[919-936]

### Suggested change
- Replace the `Directory.Exists(subDir)` gate with a stronger “is initialized and usable” check, e.g.:
 - Require either `File.Exists(Path.Combine(subDir, ".git"))` or `Directory.Exists(Path.Combine(subDir, ".git"))` before invoking git, **and/or**
 - Run a cheap probe like `git -C subDir rev-parse --is-inside-work-tree` and `continue` on failure.
- Keep the behavior as “skip” (not fail) when the submodule is uninitialized.

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



Remediation recommended

2. Redundant deleted listing call ✓ Resolved 🐞 Bug ➹ Performance
Description
ReadSourceManifestAsync now builds deletedPaths from deletedRecords, but still runs a separate
git ls-files --deleted whose output is unused, adding an extra git process and leaving dead code
in a hot path.
Code

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[R942-944]

+        var deletedPaths = deletedRecords
+            .Select(static path => Convert.ToBase64String(path))
            .ToHashSet(StringComparer.Ordinal);
Evidence
The code already has deletedRecords from RunLsFilesPrefixedAsync(... --deleted ...) and uses
that to build deletedPaths; the additional root-level deleted capture is not referenced
afterward.

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[925-944]

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

### Issue description
`ReadSourceManifestAsync` calls `git ls-files --deleted -z` into a local `deleted` byte[] that is not used anymore, because `deletedPaths` is computed from `deletedRecords`.

### Issue Context
This method is called at least twice per sync (initial + final manifest), so an unnecessary git process here is measurable overhead.

### Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[925-944]

### Suggested change
- Delete the unused `deleted` variable and its `RunGitCaptureBytes(... "ls-files", "--deleted", "-z")` call.
- Keep `deletedPaths` sourced from `deletedRecords` (which already includes submodule-prefixed deletions).

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


3. Submodule path decode unguarded ✓ Resolved 🐞 Bug ☼ Reliability
Description
ReadSourceManifestAsync decodes submodule prefixes via StrictUtf8.GetString(prefix) before the
manifest loop and without the DecoderFallbackException guard used later, so a non‑UTF8 submodule
path can escape as a raw decoding exception and break the classify-before-decode error contract.
Code

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[R929-932]

+            // Contained BEFORE the submodule's own git is invoked: the path comes from the index,
+            // and a hostile one must not steer `git -C` outside the source tree.
+            var subDir = ContainedPath(source, NormalizeRelativePath(StrictUtf8.GetString(prefix)));
+            if (!Directory.Exists(subDir)) continue;   // uninitialized submodule: nothing checked out
Evidence
The file explicitly documents keeping submodule paths undecoded, but ReadSourceManifestAsync
decodes the prefix without a guard; later, decoding manifest entries is guarded and normalized into
a specific InvalidOperationException.

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[875-877]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[928-933]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[961-966]

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

### Issue description
`StrictUtf8.GetString(prefix)` is used to derive `subDir` from an index-provided submodule path, but this decode is not wrapped in the same `DecoderFallbackException` handling as the later manifest entry decode. That violates the intended “classify-before-decode” flow and can leak an unexpected exception type.

### Issue Context
The code comments state submodule prefixes are intentionally kept as raw bytes until the manifest loop decode/guards.

### Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[875-892]
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[928-936]
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[961-966]

### Suggested change
- Wrap `StrictUtf8.GetString(prefix)` in a `try/catch (DecoderFallbackException ex)` and rethrow as the lane’s standard error (e.g. `InvalidOperationException("borrowed_snapshot_invalid_path_encoding", ex)`), **or** skip the submodule prefix in this specific failure mode.
- Keep behavior consistent with how invalid UTF-8 paths are handled in the main manifest loop.

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


4. Verbose submodule snapshot comment ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
A long narrative comment block was added where clearer naming and smaller targeted comments would
suffice, making the code harder to scan and maintain. This conflicts with the guideline to prefer
self-explanatory code over verbose comments.
Code

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[R842-845]

+            // Submodules are snapshotted as PLAIN CONTENT. Borrowing means the reviewer sees the
+            // developer's working tree, and for a submodule that is the checked-out files — not the
+            // pinned commit, which a fresh clone would give instead. Their .git is never copied: it
+            // is a gitlink file into the superproject's .git/modules, which this snapshot does not
Evidence
PR Compliance ID 8 requires concise, necessary comments and preference for self-explanatory code.
The added comment at the referenced lines is a long narrative explanation (multiple lines) rather
than a minimal clarification of intent.

CLAUDE.md: Prefer self-explanatory code over verbose comments and avoid Linear issue numbers in code comments
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[842-846]

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

## Issue description
A verbose multi-line explanatory comment was added describing submodule snapshot behavior. The compliance guideline prefers self-explanatory code and concise, necessary comments.

## Issue Context
This block reads like extended design documentation and increases noise in a hot code path.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[842-846]

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


View more (2)
5. Verbose test XML summary ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test includes an unusually long XML doc comment that reads like a design/history write-up,
which adds maintenance overhead. This conflicts with the guideline to keep comments concise and rely
on self-explanatory code/tests.
Code

test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[R244-247]

+    /// <summary>
+    /// A superproject with a submodule must snapshot. The snapshot lane refused any index carrying a
+    /// gitlink, which made borrowed review impossible on every repo that uses one — kcap-server pins
+    /// its CLI that way, so our own primary repo could not be reviewed by any snapshot vendor.
Evidence
PR Compliance ID 8 requires concise, necessary comments and preference for self-explanatory code.
The added XML summary/comment block is lengthy and includes extensive narrative context rather than
a minimal statement of the test’s intent.

CLAUDE.md: Prefer self-explanatory code over verbose comments and avoid Linear issue numbers in code comments
test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[244-253]

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 new test method has a lengthy XML documentation block that goes beyond brief intent/behavior description.

## Issue Context
Tests typically benefit from short summaries (what is asserted and why) rather than extended background.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[244-253]

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


6. Test leaves temp repos ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
BorrowedSnapshot_CarriesSubmoduleFilesAsPlainContentWithoutItsGit discards the upstream paths
returned by MakeUpstreamWithSideRef and only deletes root, leaking the helper-created temp
upstream/clone repositories across test runs.
Code

test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[R256-258]

+        var (_, sub)   = MakeUpstreamWithSideRef("refs/pull/91/head", out _);
+        var (_, super) = MakeUpstreamWithSideRef("refs/pull/92/head", out _);
+        var root = Path.Combine(Path.GetTempPath(), "kcap-borrowed-sub-" + Guid.NewGuid().ToString("N")[..8]);
Evidence
The helper allocates temp directories for upstream and clone repos, and other tests delete them; the
new test keeps only sub/super and cleans up only root, leaving the helper-created directories
behind.

test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[14-52]
test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[80-104]
test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[255-291]

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 new test creates two git fixtures via `MakeUpstreamWithSideRef` but discards the returned upstream paths and never deletes either repo directory, unlike other tests in the file.

### Issue Context
`MakeUpstreamWithSideRef` creates temp directories under `Path.GetTempPath()` for both `upstream` and `clone`. Without cleanup, repeated runs (local + CI) accumulate directories and can exhaust disk.

### Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[14-52]
- test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs[255-291]

### Suggested change
- Capture both tuples, e.g. `(subUpstream, sub)` and `(superUpstream, super)`.
- In the test’s `finally`, best-effort delete `subUpstream`, `sub`, `superUpstream`, and `super` (matching the cleanup pattern used by other tests in this file).

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


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs Outdated
Comment thread test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs
Comment thread test/Capacitor.Cli.Tests.Unit/WorktreeManagerTests.cs Outdated
Qodo caught a real bug in my own guard. `git submodule` leaves an EMPTY
directory for an uninitialized submodule, so Directory.Exists is true there,
`git -C` fails, and the whole snapshot aborts — the exact opposite of the skip
that guard was written to perform. Gated on the submodule's own .git instead.

Also from review:
- dropped the now-dead duplicate `ls-files --deleted` call, whose output stopped
  being read when deletedRecords replaced it
- the submodule test now removes the helper-created upstream/clone repos, not
  just the worktree root
- trimmed the narrative comment blocks per the repo's minimal-comments rule
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

All Qodo findings addressed.

🐞 #1 Uninitialized submodule aborts the snapshot — real, and my bug. git submodule leaves an empty directory for an uninitialized submodule, so Directory.Exists(subDir) is true, git -C there fails, and RunGitCaptureBytes's clean-exit requirement aborts the entire snapshot — the exact opposite of the skip that guard was written to perform. Now gated on the submodule's own .git (file or dir) instead of on directory existence. Good catch; the guard read as correct and wasn't.

🐞 #5 Redundant ls-files --deleted — real, removed. Its output stopped being read when deletedRecords replaced it; I left the call behind. Dead git process per snapshot, now gone.

🐞 #6 Test leaked temp repos — real, fixed. The submodule test now removes both helper-created upstream/clone pairs alongside the worktree root.

🐞 #4 Unguarded submodule path decode — already fixed before this run. Qodo reviewed 190a7a5, which predates the copilot-review commit; that commit wrapped the decode in the coded borrowed_snapshot_invalid_path_encoding. No further change needed.

📘 #2, #3 Verbose comments — trimmed, in both the production block and the test's doc comment.

WorktreeManagerTests 23 total / 0 failed.

@realtonyyoung
realtonyyoung merged commit 536b280 into main Aug 7, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/ai-1794-snapshot-submodules branch August 7, 2026 19:08
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