fix(worktree): stop sharing node_modules that links back into the source repo - #4626
fix(worktree): stop sharing node_modules that links back into the source repo#4626Yeachan-Heo wants to merge 20 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
0422d38 to
1c0ce5a
Compare
|
Review requested: independent exact-head approval needed to satisfy the PR contract. @probepark @snowykr — this PR is the fix for #4620 (owner-authored by Yeachan-Heo, so it cannot be self-approved). The PR contract requires a What to review — one function boundary in
Evidence at this exact head (all run locally by the agent on
CI on this head: all other checks pass; only the two contract gates are red, both because On approval I will update the verdict line to 🤖 Generated with Gajae Code gaebal-gajae |
There was a problem hiding this comment.
Verdict: Request changes
Summary
Reviewed the exact head: 1c0ce5a7579c2f978486b53d6dd723d7b5be44a2.
The workspace-isolation direction is sound, and the new coverage exercises the normal Bun/npm/pnpm layouts. However, two remaining failure paths can preserve or recreate the cross-checkout node_modules coupling that this PR is intended to eliminate.
Required changes
1. Fail closed when a directory cannot be inspected
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:498-504
nodeModulesLinksInto() is intended to make a fail-closed safety decision, but it swallows every readdirSync() failure and continues scanning. If a nested directory containing a workspace link is unreadable (EACCES / EPERM) or otherwise transiently unavailable, the scan can return false and ensureReusableNodeModules() will share the origin tree.
That recreates both failure modes from #4620:
- workspace imports can resolve to the origin checkout; and
- a worktree-side install can mutate the origin
node_modules.
Required fix: Treat an unreadable traversal directory as unsafe and isolate the worktree. Add a regression test for an unreadable traversal path.
2. Remediate pre-existing external-hoist links
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:552-562
A worktree created before this change may already contain:
worktree/node_modules -> source/node_modules
When source/node_modules is itself a parent-workspace hoist, both paths resolve outside sourceRoot. resolvesInside() therefore returns false, the function returns "present", and the stale cross-checkout link remains active—even though new worktrees correctly refuse to create that link.
Required fix: Recognize a stale worktree link by matching it to the source node_modules identity, rather than only checking whether its resolved destination is inside sourceRoot. Add a reuse regression for a nested repository whose source node_modules is a parent-workspace hoist.
Evidence reviewed
| Area | Result |
|---|---|
| Intent / contract | The proposed heuristic matches #4620's stated direction. |
| Correctness / security | The two failure paths above remain blocking. |
| Tests / CI | Affected-path checks are green. The two PR-contract checks are red because the PR body intentionally declares needs-human, not because a test failed. |
| Compatibility | Launch and SDK callers share the updated helper; the remediation behavior still needs the external-hoist reuse case above. |
I did not execute PR code as part of this review.
…t links Review follow-up to snowykr's exact-head review of #4626 (two required changes): 1. nodeModulesLinksInto swallowed every readdirSync failure and kept scanning, so a workspace self-link hidden behind an unreadable directory could still pass the scan and the origin tree would be shared — recreating both #4620 failure modes. Unreadable traversal directories now fail closed (treated as unsafe); only a directory that vanished mid-scan is skipped. 2. Remediation of stale cross-checkout links only recognized links resolving inside sourceRoot. A worktree link to a source node_modules that is itself a parent-workspace hoist resolves outside the repo entirely, so the pre-fix contaminated link stayed active. Remediation now also matches the source node_modules by resolved identity, removing the stale link regardless of where the hoisted tree lives. Both paths carry the requested regressions: an unreadable scan directory and a stale parent-hoist worktree link. Lore-id: e06d4dc2 Constraint: identity-match remediation only removes symlinks resolving to the source's own node_modules Constraint: only ENOENT directories are skippable during the scan Confidence: high Scope-risk: narrow Reversibility: single-commit on top of the main fix Tested: 41-test launch-worktree suite; targeted probes for both review cases; dogfood dist/gjc --worktree Not-tested: EPERM-vs-EACCES distinction on exotic filesystems
|
@snowykr — both required changes from your review are implemented and pushed at the new head 1. Fail closed on unreadable traversal directories — 2. Remediate pre-existing external-hoist links — Both requested regressions added (
Full verification at this head: 41/41 Verdict digest updated to the new exact head: 🤖 Generated with Gajae Code gaebal-gajae |
snowykr
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Summary
Reviewed the follow-up on exact head 22eab0ea0b92093c7803a41b092489e5ec82532a.
The two previous changes are addressed on their normal paths:
- traversal now isolates when
readdirSync()cannot inspect a directory; and - stale links to a parent-workspace hoist are recognized by resolved
node_modulesidentity.
Two issues still need resolution before this can be approved.
Required changes
1. Keep stale-hoist remediation fail-closed on realpath errors
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:589-599
resolvesToSourceModules() catches every realpathSync() error and returns false. For the case this helper was added to fix, resolvesInside() is already false because the parent hoist is outside sourceRoot; a permission or I/O failure resolving either path therefore leaves the pre-existing cross-checkout link classified as present.
That is fail-open behavior for an unreadable parent-workspace hoist and can preserve the #4620 coupling until filesystem permissions happen to change.
Required fix: Mirror the scan's safety rule: only proven broken-link errors may return false; treat permission and other resolution failures as unsafe and remove/isolate the stale worktree link. Add a regression that injects EACCES or EPERM from this identity-resolution path.
2. Make the unreadable-directory regression deterministic
Location: packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts:623-637
The new test relies on chmod(0o000) making readdirSync() fail. That is not reliable when the suite runs as root, which bypasses permission bits: the scanner then sees the workspace symlink normally and isolates even if the new error-handling branch regresses. The test passes without proving the fail-closed behavior it is intended to protect.
Required fix: Fault-inject EACCES/EPERM from readdirSync() (for example with a scoped spy) and assert that the scan isolates. Keep the filesystem-level case only as supplemental platform coverage if useful.
Evidence reviewed
| Area | Result |
|---|---|
| Intent / contract | The follow-up remains scoped to the requested isolation and remediation behavior. |
| Correctness / trust | The normal unreadable-scan and parent-hoist paths are improved; the unreadable identity-resolution path remains fail-open. |
| Tests / CI | The exact-head affected-path test, type-build, native-build, and integration checks are green. The two PR-contract jobs are red because the PR body still declares needs-human, not because a test failed. |
| Compatibility | The stale parent-hoist positive case is now covered; permission-restricted filesystems still need the remediation behavior above. |
I reviewed the checked-out PR source directly and did not execute PR code for this review.
…ic EACCES regressions Second review follow-up to snowykr's review of #4626: 1. resolvesToSourceModules swallowed every realpathSync error and returned false, so a stale cross-checkout link behind a permission-restricted parent-workspace hoist stayed classified as present — fail-open for exactly the case the helper was added to remediate. It now mirrors the scan's rule: only proven-broken-link failures (ENOENT/ENOTDIR/ELOOP/EDEADLK) may return false; EACCES and any other resolution failure treat the link as unsafe and isolate. The outer sourceRoot realpath is tolerated the same way so one unreadable path cannot crash the launch. 2. The unreadable-directory regression relied on chmod(0o000), which root bypasses — the test could pass without exercising the fail-closed branch. Replaced with deterministic fault injection: a scoped readdirSync spy throwing EACCES, plus a new realpathSync-failure regression for the stale-link identity path. Lore-id: d561d95c Constraint: identity-resolution failures must isolate, never classify present Constraint: EACCES regressions must be spy-injected, not permission-bit dependent Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 42-test launch-worktree suite incl. both injected-fault regressions; sdk-broker-lifecycle-e2e 82/82 Not-tested: exotic I/O error codes beyond EACCES injection
|
@snowykr — both items from your second review are implemented and pushed at the new head 1. Identity resolution now fails closed — 2. Deterministic EACCES regressions — the chmod-based test is replaced with fault injection:
Verification at this head: 42/42 🤖 Generated with Gajae Code gaebal-gajae |
snowykr
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Summary
Reviewed the latest exact head: 8fe787505bcbc94f476b9e190635fb2de374482e.
The new implementation improves the error classification, and the intended fail-closed behavior is present on the normal paths. However, the latest follow-up still has correctness and verification gaps that can either leave the worktree coupled to an ancestor checkout or make the new regression tests pass without exercising the changed branches.
Required changes
1. Do not rely on an ancestor node_modules when isolation returns missing
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:581
When the source tree is rejected, the function returns "missing" without creating any local node_modules boundary. The default launch worktree is a sibling under the repository parent, so Node/Bun resolution can walk up to <parent>/node_modules. The existing parent-workspace fixture creates exactly that layout (parent/node_modules/@scope/app -> parent/packages/app). The launched worktree can therefore still resolve the parent workspace's live sources despite this isolation check.
Required fix: Ensure an isolated worktree cannot fall through to an ancestor module root—for example, create an empty real node_modules boundary where appropriate, or place/reject worktrees so the relevant ancestor module root is unreachable—and add a test that verifies actual module resolution, not only that the worktree-local path is absent.
2. Make both new EACCES regressions target the changed branches
Locations: packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts:633-636, 654-660
Both fault-injection tests are currently false-positive:
- The
readdirSyncmockImplementationOnceis consumed by the scanner's first read of thenode_modulesroot, while the callback asserts that the path is@scope. That assertion error is caught by production as a non-ENOENTerror, so the test passes without reaching the intended unreadable traversal directory. - The
realpathSyncmockImplementationOnceis consumed by the outerfs.realpathSync(sourceRoot)call inensureReusableNodeModules, beforeresolvesToSourceModules()runs. The new helper's fail-closed branch is therefore not exercised; the test would still pass with the previous helper implementation.
Required fix: Preserve the real implementation for unrelated paths and inject EACCES only when the call reaches the intended scoped directory / helper resolution. Assert that the intended path was reached and that the stale link is removed.
3. Do not delete user-owned links when identity resolution is merely unknown
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:570-575
The new fail-closed resolvesToSourceModules() result is used directly to call rmSync(target). Any non-broken resolution error (EACCES, EPERM, EIO, or a transient network-filesystem failure) now causes an existing worktree symlink to be deleted, even though its target could be a user-owned external/global store and launcher ownership was not established.
Required fix: Preserve the fail-closed safety decision without destructive cleanup: either fail the launch while retaining an unresolved user-owned entry, or remove only an exact launcher-created link whose identity is positively verified. Add a regression for an inaccessible unrelated external symlink.
4. Use filesystem-aware identity comparisons
Locations: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:531-533, 606-609
resolvesInside() and resolvesToSourceModules() compare raw realpathSync() strings case-sensitively. On case-insensitive Windows and default case-insensitive macOS volumes, the same physical path can be returned with different casing or alias spelling. A self-link or stale source node_modules junction can then be classified as unrelated and retained as "present".
Required fix: Compare canonical filesystem identities (or use platform/filesystem-aware path normalization) before allowing reuse, and add platform coverage for case/alias differences.
Evidence reviewed
| Area | Result |
|---|---|
| Intent / contract | The latest scope remains aligned with the requested isolation fix. |
| Correctness / security | Ancestor module fallback, destructive uncertainty handling, and path identity remain blocking. |
| Tests / CI | Exact-head targeted build/test jobs are present, but the two new EACCES tests are not branch-specific. The affected-path aggregate also failed because its evidence producer could not download the referenced plan artifact; this is CI infrastructure evidence, not a demonstrated product-test failure. |
| Compatibility | Windows/macOS identity behavior and parent-workspace module resolution need explicit coverage. |
I reviewed the actual checked-out PR branch at the exact head and did not execute PR code as part of this review.
…emediation Third review follow-up to snowykr's review of #4626 (four required changes): 1. An isolated worktree could still fall through to an ancestor module root. Launch worktrees are siblings under the repository's parent, so a nested repo inside a parent workspace resolves the parent's live workspace sources even after sharing is refused — empirically, an empty node_modules does NOT stop Node/Bun walk-up. Isolation now creates a worktree-local boundary: every workspace member declared by the worktree's own root package.json is linked into worktree/node_modules/<name> -> <worktree member>, mirroring what a worktree-local install creates for workspace packages (offline, deterministic, own-commit only). External deps remain the user's install to create; it is free to replace these links. 2. The two EACCES regressions were not branch-targeted: the first-call spies were consumed by earlier calls before reaching the intended directory/helper. Both now preserve the real implementation for unrelated paths, inject only at the guarded location, and assert the intended path was reached. 3. Removal of a stale worktree link no longer trusts fail-closed verdicts: remediation requires positive proof (positivelyResolves Inside / positivelyResolvesToSourceModules). An unresolvable link (EACCES/EPERM/EIO, transient failures) could be user-owned, so it is classified present and left for the user to inspect. Fail-closed semantics remain on the read-only share decision. 4. Path identity comparisons are filesystem-aware: resolvesInside and the identity helpers compare case-insensitively on win32/darwin so alias-spelled or differently-cased paths are recognized instead of classified unrelated. Lore-id: d7e82b3e Constraint: boundary links point only at the worktree's own packages, never another checkout Constraint: destructive remediation requires positive identity proof, never a fail-closed guess Rejected: empty node_modules boundary | empirically does not stop Node/Bun ancestor walk-up Rejected: reject launches when an ancestor node_modules exists | breaks legitimate nested-repo workflows Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 44-test launch-worktree suite incl. real module-resolution probe and non-deletion regression; harness e2e 13/13; dogfood dist/gjc --worktree resolves own commit pre-install Not-tested: real Windows/macOS case-variant filesystems (CI covers linux)
|
@snowykr — all four items from your third review are implemented and pushed at the new head 1. Ancestor module-root fall-through — isolation now builds a worktree-local boundary: every workspace member declared by the worktree's own root 2. Branch-targeted fault injection — both EACCES regressions now preserve the real implementation for unrelated paths and inject only at the guarded location, asserting the intended path was reached ( 3. No destructive cleanup on unproven identity — remediation now requires positive proof: new 4. Filesystem-aware identity — Verification at this head: 44/44 🤖 Generated with Gajae Code gaebal-gajae |
|
Exact-head CI triage for Dependency chain:
Current disposition: reviewer-owned hold / OWNER_CONFIRMATION_REQUIRED. Requested reviewers are — |
|
Follow-up on exact-head run The required focused shard
Consequently — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Summary
Reviewed the latest follow-up at exact head d4af0a7c1ea936e7d7d8f392311f580dd5d33552 using the checked-out PR branch and independent reviews across contract, correctness, security, verification, and platform compatibility.
The new worktree-local boundary is a meaningful improvement, but the implementation still has several paths that can preserve ancestor-checkout resolution or perform destructive/unconfined filesystem operations.
Required changes
1. Reconcile existing worktree boundaries instead of treating any real directory as isolated
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:563-603
A non-symlink worktree/node_modules returns "present" immediately, so an empty or partial directory created by an earlier launch is never populated or validated against the current worktree commit. The PR's own rationale correctly notes that an empty local directory does not stop Node/Bun from walking up to an ancestor node_modules.
After a reused worktree is checked out at a new commit, stale member links can remain while new workspace members are absent; those missing imports can resolve to a parent workspace's live sources.
Required fix: Track launcher-owned boundary state and reconcile it on every reuse/checkout, or fail closed when ownership/completeness cannot be proven. Add a regression that changes workspace members across worktree reuse.
2. Fail closed on incomplete workspace discovery and support pnpm declarations
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:626-656
ensureWorkspaceSelfLinkBoundary() only reads package.json.workspaces. It does not handle the common pnpm-workspace.yaml-only declaration, and it silently skips unreadable or invalid root/member manifests. The caller ignores the boolean result and proceeds.
For a nested repository beside a parent workspace, this can leave no or only a partial local boundary, allowing imports to fall through to the parent's live packages—the exact #4620 failure mode.
Required fix: Parse the authoritative workspace declarations for supported package managers, including pnpm patterns/exclusions, and treat unreadable/invalid manifests or incomplete member enumeration as an actionable isolation failure rather than successful launch.
3. Positive target identity is not proof of launcher ownership
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:580-584
The remediation path removes any worktree node_modules symlink whose resolved target is anywhere inside sourceRoot. That proves the destination, but not that GJC created the link. A user-owned link such as:
worktree/node_modules -> sourceRoot/packages/custom-node-modules
will be deleted on the next launch. The same ownership ambiguity applies to the parent-hoist identity path.
Required fix: Remove only an exact, positively authorized launcher-created link (including the known source node_modules identity), or persist an explicit ownership marker and use identity-bound removal. Do not equate source containment with ownership.
4. Do not continue execution with an unproven or dangling node_modules link
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:563-589
When positive identity resolution fails with EACCES/EPERM/I/O errors, the link is kept and the function returns "present"; launch continues through an unresolved dependency boundary that may still point at the origin checkout. Conversely, proven dangling links (ENOENT/ENOTDIR/ELOOP) are also left present, allowing resolution to continue to ancestor module roots.
Required fix: Preserve an unprovable user-owned link only by refusing the launch with an actionable error. Remediate proven dangling launcher links safely, and add regressions for both cases.
5. Validate workspace manifest paths and package names before writing links
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:638-668
Workspace patterns, discovered member paths, and package names are treated as trusted. A traversal/absolute pattern or a package name containing .. can make mkdirSync/symlinkSync operate outside worktree/node_modules; member directories are also not checked by realpath containment. This turns repository-controlled metadata into an escape from the intended worktree boundary.
Required fix: Reject traversal/absolute patterns, validate package-name grammar (including Windows separators), and verify both manifest/member realpaths and final link paths remain inside the intended worktree/module roots before mutation.
6. Fix the external-link EACCES regression test and refresh exact-head evidence
Location: packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts:796-803
The new external-link test uses mockImplementationOnce for realpathSync, but the first call during reuse resolves sourceRoot, before the target link is examined. The mock is therefore consumed by the wrong call and the test can pass through the ordinary dangling-link ENOENT path without exercising the changed positive-proof branch.
Required fix: Use a persistent target-aware spy that delegates to the real implementation for unrelated paths, assert that the target path was reached, and then assert that the unprovable external link remains.
The exact-head affected test job for run 32042806762 also failed during setup while downloading an action (HTTP 429) before executing the test shard, while the PR contract gate intentionally rejects the current needs-human verdict. Re-run the affected checks after the test fix before treating the local claims as exact-head CI evidence. The PR description's 39 pass evidence is also stale relative to this head's 44 test cases.
Evidence reviewed
| Area | Result |
|---|---|
| Intent / contract | The follow-up addresses the requested boundary direction, but ownership and complete workspace-discovery contracts remain unmet. |
| Correctness / failure | Existing/partial boundaries, unknown links, dangling links, and worktree reuse can still permit ancestor resolution. |
| Security / trust | Cleanup ownership and manifest-driven path containment remain blocking. |
| Verification / CI | The targeted exact-head run did not reach the test shard; the new external-link EACCES test is not branch-specific. |
| Platform / compatibility | pnpm-only workspaces and filesystem-specific identity/UNC behavior still need explicit handling or a documented fail-closed policy. |
I reviewed the actual checked-out PR branch at the exact head and did not execute PR code as part of this review.
…lidation Fourth review follow-up to snowykr's review of #4626 (six required changes): 1. Boundary reconciliation: a launcher-owned boundary directory now carries a .gjc-node-modules-boundary marker. Every launch reconciles it against the current commit's workspace members — stale member links from a previous commit are pruned and new members linked — so a reused worktree checked out at a different commit can never resolve deleted members through an ancestor node_modules. 2. Complete workspace discovery with fail-closed semantics: the boundary reads package.json workspaces (array and {packages}) AND pnpm-workspace.yaml (via Bun.YAML). Unreadable or malformed root or member manifests are actionable isolation failures (worktree_workspace_manifest_unreadable / _member_invalid), never silent skips. 3. Ownership: remediation removes only positively-identified launcher-created links (target equal to the source checkout's own node_modules) and marker-owned directories. Source containment alone no longer authorizes deletion. 4. No execution through unproven boundaries: a worktree node_modules link whose resolution fails (EACCES/EPERM/EIO/transient) fails the launch with worktree_node_modules_unverified instead of continuing; the link is preserved for the user. Proven dangling links are removed and replaced with the boundary. A link that resolves outside the source tree is provably user-owned external and stays present. 5. Manifest path/name validation: workspace patterns must be relative without traversal segments (worktree_workspace_pattern_unsafe), member names must match npm package grammar including a Windows-separator check (worktree_workspace_member_name_invalid), and both member manifests and final link paths are verified to stay inside the worktree before any mutation. 6. The external-link EACCES regression now uses a persistent target-aware spy that delegates to the real implementation for unrelated paths and asserts the target was reached before asserting the refusal. Lore-id: 051c42a0 Constraint: only marker-owned directories and positively-identified launcher links are ever removed Constraint: workspace discovery must be complete or the launch fails with an actionable typed error Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 50-test launch-worktree suite (8 new: reconciliation, pnpm-yaml, traversal/name/manifest refusals, user-dir preservation); dogfood with marker + reconciliation Not-tested: real pnpm install replacing the boundary end-to-end
|
@snowykr — all six items from your fourth review are implemented and pushed at the new head 1. Boundary reconciliation instead of trusting any real directory — the boundary directory now carries a 2. Complete discovery, pnpm support, fail-closed manifests — 3. Ownership, not containment — remediation removes only (a) symlinks whose target positively equals the source checkout's own 4. No execution through unproven/dangling links — a link whose resolution fails (EACCES/EPERM/EIO/transient) throws 5. Manifest path/name validation — patterns must be relative without 6. Persistent target-aware EACCES spy — the external-link regression now delegates to the real implementation for unrelated paths (the first reuse call resolves Also per your note: the failed setup was GitHub infra (429 on action downloads); the checks were rerun and the exact-head run is green for all product jobs. Evidence at this head: 50/50 🤖 Generated with Gajae Code gaebal-gajae |
snowykr
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Summary
Reviewed the latest follow-up at exact head c07f76b6a47691c6e5b1139b6c92f4bf85cb9eb4 using the checked-out PR branch and independent reviews across contract, correctness, security, verification, and platform compatibility.
The marker-owned/reconciled boundary is a substantial improvement, but the implementation still has isolation, ownership, and verification gaps that block approval.
Required changes
1. Do not treat every existing real directory as a safe boundary
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:617-625, 630-643
An unmarked real worktree/node_modules directory returns "present" without scanning or establishing a workspace boundary. Likewise, a workspace repository whose origin node_modules contains only external/partial dependencies can pass the scan and be shared before the workspace declaration is consulted.
An empty/partial/legacy directory can therefore miss own-commit workspace links and let Node/Bun resolve the missing package from an ancestor workspace checkout, recreating #4620.
Required fix: Select the isolation path from the authoritative workspace declaration before sharing, and either verify a complete safe install or fail closed. Preserve user-owned directories, but do not silently claim they isolate the worktree.
2. Directory-level marker ownership is insufficient for child-link reconciliation
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:737-768
The marker proves only that the boundary directory was once created by GJC. Reconciliation then removes or replaces child symlinks without proving that GJC created those individual entries:
- expected links are removed before recreation; and
- every non-expected scoped symlink is pruned.
The implementation also says package-manager installs may replace the boundary links while leaving the marker in place. A later launch can therefore delete user/package-manager dependency links that happen to live under the marked directory.
Required fix: Record and verify per-link launcher ownership/target identity, or preserve unknown links and fail closed. A directory marker alone must not authorize descendant deletion.
3. Do not delete user-owned dangling links based only on broken realpath
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:589-595, 648-654
isProvenDangling() treats ENOENT, ENOTDIR, ELOOP, and EDEADLK as proof that the launcher may remove the link. These errors prove only that the target is currently unavailable—not that GJC created the symlink. This can delete a user-owned offline/global/UNC store link.
Required fix: Preserve and refuse unowned dangling links unless a durable launcher record positively identifies the exact entry. Only proven launcher-owned links may be auto-removed.
4. Enforce realpath/no-follow containment before boundary mutation
Location: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:705-743, 839-842
isInsideDirectory() is lexical only. Existing symlinked/junctioned parents such as node_modules/@scope -> /outside can pass the lexical check; subsequent mkdirSync, rmSync, and symlinkSync follow those parents and mutate outside the worktree. A glob-discovered member directory can likewise be a symlink to an external checkout.
Required fix: Validate canonical member/manifest identities and every destination parent with no-follow/realpath-aware checks immediately before mutation, or reject symlinked parents and member directories outright. Add a regression proving no outside sentinel is changed.
5. Make workspace declaration and reconciliation complete
Locations: packages/coding-agent/src/gjc-runtime/launch-worktree.ts:685-689, 780-829
The boundary returns without reconciliation when the current commit has no recognized workspace declaration, leaving old member links/marker state behind. It also returns early on package.json.workspaces and never merges or validates a coexisting pnpm-workspace.yaml; malformed field shapes and non-string entries are coerced or treated as absent.
This leaves incomplete member sets and can reopen ancestor resolution after a workspace-to-nonworkspace or dual-manifest checkout transition.
Required fix: Reconcile the empty set for a marker-owned boundary, validate declaration types strictly, and merge or reject conflicting package-manager declarations deterministically. Treat inaccessible/ambiguous metadata as an isolation failure.
6. Exact-head verification is not yet substantiated by the changed CI plan
Locations: packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts:529, 540, 552, 563, 573, 585, 605, 628, 840; .github/workflows/dev-ci.yml / scripts/ci-dev-affected.ts
Several new absence assertions use BunFile.exists(), which follows symlinks and cannot distinguish a missing entry from a real directory, valid directory symlink, or dangling link. The reconciliation and stale-link tests can therefore pass without proving the directory-entry state they claim to test.
The current exact-head checks do not provide a completed affected test/check receipt for this head, while the PR contract remains failed because the body still declares needs-human. The local claims of 50/50 tests, 82/82 broker e2e, package checks, build, and dogfood are not yet equivalent to immutable exact-head CI evidence.
Required fix: Use lstat/readlink/realpath assertions for link-vs-directory state, complete the exact-head affected test and package-check evidence, and refresh the PR verification text only after those artifacts exist.
Platform note
The new code still uses "junction" for boundary links and assumes Darwin is case-insensitive. No real Windows/macOS launch-worktree coverage is present in the affected CI path. Either add platform-specific coverage or explicitly reject unsupported UNC/case-sensitive filesystem cases with actionable errors.
Evidence reviewed
| Area | Result |
|---|---|
| Intent / contract | Marker/reconciliation direction is aligned, but ownership and complete declaration handling remain unresolved. |
| Correctness / failure | Partial/legacy boundaries, no-declaration transitions, and unowned links can still preserve unsafe resolution. |
| Security / trust | Lexical containment and descendant deletion remain unsafe under symlinked parents or package-manager replacement. |
| Verification / CI | New tests contain link-state false positives; exact-head affected evidence is not complete. |
| Platform / compatibility | UNC/junction and real case-sensitive filesystem behavior remain unverified. |
I reviewed the actual checked-out PR branch at the exact head and did not execute PR code as part of this review.
…rations Fifth review follow-up to snowykr's review of #4626 (six required changes): 1. Declaration-driven isolation: the share decision consults the worktree's own workspace declaration before the origin link scan, so a workspace repo is never shared even when its origin tree merely happens to contain no self-links (external hoists, partial installs). An unmarked real directory remains user-owned and untouched. 2. Per-link ownership: the boundary records every launcher-created member link in .gjc-node-modules-links.json. Reconciliation replaces and prunes only recorded links; a package-manager install that replaced the tree leaves no recorded ownership, so its entries are never touched even if the marker survives. A directory marker alone no longer authorizes descendant deletion. 3. Dangling links are never auto-removed: a broken resolution proves unavailability, not ownership, so dangling/unreadable links fail the launch with worktree_node_modules_unverified and are preserved for the user. Only a link positively resolving to the source checkout's own node_modules — the exact link this launcher creates — is remediated. 4. Realpath-aware containment: member manifests and member directories must canonically resolve inside the worktree (symlinked members pointing at another checkout are rejected); link destinations use path containment with a symlinked-parent realpath check. Found and fixed via dogfood: containment for link DESTINATIONS must be path-based because the target legitimately lives in packages/. 5. Complete declaration handling: pnpm-workspace.yaml and package.json workspaces are strictly typed (non-string or non-array shapes are isolation failures), merged deterministically when both exist, and a workspace-to-non-workspace transition reconciles the boundary to empty and drops the marker. 6. Entry-state assertions: boundary tests assert lstat link/dir/missing states instead of Bun.file().exists(), and the external-link EACCES regression uses a persistent target-aware spy with a reached flag. Lore-id: 43ec1f3d Constraint: only recorded launcher links are ever replaced or pruned Constraint: link-destination containment is path-based; member containment is realpath-based Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 53-test launch-worktree suite (3 new: pkg-replaced preservation, declaration-gone reconciliation, symlinked-parent rejection); dogfood across relaunches caught and fixed the reuse containment crash Not-tested: real Windows junction/UNC paths (CI covers linux)
|
@snowykr — all six items from your fifth review are implemented and pushed at the new head
Evidence at this head: 53/53 🤖 Generated with Gajae Code gaebal-gajae |
…t links Review follow-up to snowykr's exact-head review of #4626 (two required changes): 1. nodeModulesLinksInto swallowed every readdirSync failure and kept scanning, so a workspace self-link hidden behind an unreadable directory could still pass the scan and the origin tree would be shared — recreating both #4620 failure modes. Unreadable traversal directories now fail closed (treated as unsafe); only a directory that vanished mid-scan is skipped. 2. Remediation of stale cross-checkout links only recognized links resolving inside sourceRoot. A worktree link to a source node_modules that is itself a parent-workspace hoist resolves outside the repo entirely, so the pre-fix contaminated link stayed active. Remediation now also matches the source node_modules by resolved identity, removing the stale link regardless of where the hoisted tree lives. Both paths carry the requested regressions: an unreadable scan directory and a stale parent-hoist worktree link. Lore-id: e06d4dc2 Constraint: identity-match remediation only removes symlinks resolving to the source's own node_modules Constraint: only ENOENT directories are skippable during the scan Confidence: high Scope-risk: narrow Reversibility: single-commit on top of the main fix Tested: 41-test launch-worktree suite; targeted probes for both review cases; dogfood dist/gjc --worktree Not-tested: EPERM-vs-EACCES distinction on exotic filesystems
…ic EACCES regressions Second review follow-up to snowykr's review of #4626: 1. resolvesToSourceModules swallowed every realpathSync error and returned false, so a stale cross-checkout link behind a permission-restricted parent-workspace hoist stayed classified as present — fail-open for exactly the case the helper was added to remediate. It now mirrors the scan's rule: only proven-broken-link failures (ENOENT/ENOTDIR/ELOOP/EDEADLK) may return false; EACCES and any other resolution failure treat the link as unsafe and isolate. The outer sourceRoot realpath is tolerated the same way so one unreadable path cannot crash the launch. 2. The unreadable-directory regression relied on chmod(0o000), which root bypasses — the test could pass without exercising the fail-closed branch. Replaced with deterministic fault injection: a scoped readdirSync spy throwing EACCES, plus a new realpathSync-failure regression for the stale-link identity path. Lore-id: d561d95c Constraint: identity-resolution failures must isolate, never classify present Constraint: EACCES regressions must be spy-injected, not permission-bit dependent Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 42-test launch-worktree suite incl. both injected-fault regressions; sdk-broker-lifecycle-e2e 82/82 Not-tested: exotic I/O error codes beyond EACCES injection
|
Exact-head follow-up review request for The latest delta canonicalizes scoped link-parent directories before publication, closing the remaining mutable-parent pathname window. Existing lock successor and internal scan-retarget regressions remain green. Please review this exact head; merge remains blocked until authenticated approval and required checks are green. |
|
Exact-head reconciliation update for PR #4626:
All prior-head review and CI evidence is stale. Please review this exact head; approval remains required before merge. Fresh affected CI and adversarial review are being collected now. — |
probepark
left a comment
There was a problem hiding this comment.
Delta review at exact head cbd9f1bf — merge blocked.
The three prior identity races remain: stale-lock removal is still pathname-bound, boundary publication still follows replaceable node_modules/scope paths, and internal-symlink enumeration can occur after the validated path is swapped.
The delta adds two concrete regressions:
- An old ownerless lock can spin forever. On
owner.jsonENOENT,tryBreakBoundaryLockreturns success without removing the lock directory; the caller immediately retries before deadline/sleep and repeats indefinitely. - Supported symlink/junction worktree buckets fail for workspace boundaries because canonical node_modules paths are checked against the lexical worktree alias, producing
worktree_boundary_outside.
Realpath strings are not object identity. Use handle/inode-bound operations or fail closed on stale reclamation and internal directory symlinks; ensure ownerless-lock retries remove safely or reach a bounded wait, and canonicalize both sides for supported bucket containment.
Reviewed by @probepark — finite delta review from d1941a54, tracing every prior race plus ownerless crash-window and bucket-alias behavior.
gajae.pr-review-verdict.v1 merge-blocked sha256:3919e5db562b30fd7d6746e9be5f84868e9e5188b06e253519703b8d5a12ecd4 reviewer:human reviewer-id:probepark evidence:exact-head-cbd9f1bf-path-identity-races-ownerless-spin-and-bucket-regression
|
Exact-head fix-forward update for PR #4626:
All prior-head review and CI evidence is stale. Please review this exact head. Merge remains blocked pending independent authenticated approval and fresh required CI. — |
probepark
left a comment
There was a problem hiding this comment.
Delta review at exact head d5f65f17 — merge blocked.
launch-worktree.ts is byte-identical to blocked cbd9f1bf; the delta changes tests only. Therefore all implementation blockers remain:
- pathname-only stale-lock reclamation can delete a successor live lock
- an old ownerless lock causes an unbounded tight retry loop
- boundary publication follows replaceable node_modules/scope paths
- internal symlink traversal is not bound to validated directory identity
- workspace repositories launched through supported symlink/junction buckets fail physical-vs-lexical containment
The new tests cover already-present invalid parents and a pre-publication realpath change, not same-path replacement after validation. Runtime changes are required before another approval round.
Reviewed by @probepark — exact blob/delta comparison from cbd9f1bf; implementation unchanged.
gajae.pr-review-verdict.v1 merge-blocked sha256:a8e0f06efaed82c8c6f2b8dc04040edb60e2f4a7091a56f2659e1485294eddf9 reviewer:human reviewer-id:probepark evidence:exact-head-d5f65f17-runtime-identical-all-worktree-blockers-retained
|
Exact-head runtime fix-forward update for PR #4626:
All prior-head review evidence is stale. Please review this exact head for the five runtime blockers. — |
|
Current-dev reconciliation update for PR #4626:
All prior-head CI and review evidence is stale. Please re-review this exact current head after fresh CI completes. — |
|
Current exact-head gate status for PR #4626:
The runtime REQUEST_CHANGES findings have been fixed and pinned. Merge remains blocked only on the authenticated maintainer review and unavailable executor-role receipt. — |
…rce repo `ensureReusableNodeModules` symlinked the entire origin `node_modules` into launch worktrees. Workspace installs (bun/npm/pnpm) link `@scope/pkg` entries back into the repo that owns them, so the worktree's every workspace import resolved the origin checkout's live sources — a clean worktree at commit X could import the origin's uncommitted edits — and `bun install` run inside the worktree wrote new dependencies into the origin's tree (#4620). The launch now scans the origin tree for links resolving inside the origin repo root (hoisted, `.bun/node_modules`, `.pnpm` scoped layouts; fail-closed on unreadable links) and shares only when none exist. An origin `node_modules` that is itself a symlink is never shared: whether it points into the repo or at a parent workspace hoist, its entries belong to another checkout's install graph. Already-contaminated worktrees have the stale link removed on next launch; worktree-owned real directories are never touched. Lore-id: 1ee5b18e Constraint: remediation must only ever remove a symlink gjc itself would have created Constraint: scan stays bounded — never descends through links resolving outside node_modules Rejected: always run bun install in the worktree | launch must stay offline/fast; installs are the user's call Rejected: share only for non-workspace repos detected via package.json | node_modules link shape is the authoritative signal Confidence: high Scope-risk: narrow Reversibility: single-commit Tested: 39-test launch-worktree suite incl. 10 new isolation cases; e2e dogfood with built CLI Not-tested: Windows junction semantics on a real Windows host
…t links Review follow-up to snowykr's exact-head review of #4626 (two required changes): 1. nodeModulesLinksInto swallowed every readdirSync failure and kept scanning, so a workspace self-link hidden behind an unreadable directory could still pass the scan and the origin tree would be shared — recreating both #4620 failure modes. Unreadable traversal directories now fail closed (treated as unsafe); only a directory that vanished mid-scan is skipped. 2. Remediation of stale cross-checkout links only recognized links resolving inside sourceRoot. A worktree link to a source node_modules that is itself a parent-workspace hoist resolves outside the repo entirely, so the pre-fix contaminated link stayed active. Remediation now also matches the source node_modules by resolved identity, removing the stale link regardless of where the hoisted tree lives. Both paths carry the requested regressions: an unreadable scan directory and a stale parent-hoist worktree link. Lore-id: e06d4dc2 Constraint: identity-match remediation only removes symlinks resolving to the source's own node_modules Constraint: only ENOENT directories are skippable during the scan Confidence: high Scope-risk: narrow Reversibility: single-commit on top of the main fix Tested: 41-test launch-worktree suite; targeted probes for both review cases; dogfood dist/gjc --worktree Not-tested: EPERM-vs-EACCES distinction on exotic filesystems
…ic EACCES regressions Second review follow-up to snowykr's review of #4626: 1. resolvesToSourceModules swallowed every realpathSync error and returned false, so a stale cross-checkout link behind a permission-restricted parent-workspace hoist stayed classified as present — fail-open for exactly the case the helper was added to remediate. It now mirrors the scan's rule: only proven-broken-link failures (ENOENT/ENOTDIR/ELOOP/EDEADLK) may return false; EACCES and any other resolution failure treat the link as unsafe and isolate. The outer sourceRoot realpath is tolerated the same way so one unreadable path cannot crash the launch. 2. The unreadable-directory regression relied on chmod(0o000), which root bypasses — the test could pass without exercising the fail-closed branch. Replaced with deterministic fault injection: a scoped readdirSync spy throwing EACCES, plus a new realpathSync-failure regression for the stale-link identity path. Lore-id: d561d95c Constraint: identity-resolution failures must isolate, never classify present Constraint: EACCES regressions must be spy-injected, not permission-bit dependent Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 42-test launch-worktree suite incl. both injected-fault regressions; sdk-broker-lifecycle-e2e 82/82 Not-tested: exotic I/O error codes beyond EACCES injection
…emediation Third review follow-up to snowykr's review of #4626 (four required changes): 1. An isolated worktree could still fall through to an ancestor module root. Launch worktrees are siblings under the repository's parent, so a nested repo inside a parent workspace resolves the parent's live workspace sources even after sharing is refused — empirically, an empty node_modules does NOT stop Node/Bun walk-up. Isolation now creates a worktree-local boundary: every workspace member declared by the worktree's own root package.json is linked into worktree/node_modules/<name> -> <worktree member>, mirroring what a worktree-local install creates for workspace packages (offline, deterministic, own-commit only). External deps remain the user's install to create; it is free to replace these links. 2. The two EACCES regressions were not branch-targeted: the first-call spies were consumed by earlier calls before reaching the intended directory/helper. Both now preserve the real implementation for unrelated paths, inject only at the guarded location, and assert the intended path was reached. 3. Removal of a stale worktree link no longer trusts fail-closed verdicts: remediation requires positive proof (positivelyResolves Inside / positivelyResolvesToSourceModules). An unresolvable link (EACCES/EPERM/EIO, transient failures) could be user-owned, so it is classified present and left for the user to inspect. Fail-closed semantics remain on the read-only share decision. 4. Path identity comparisons are filesystem-aware: resolvesInside and the identity helpers compare case-insensitively on win32/darwin so alias-spelled or differently-cased paths are recognized instead of classified unrelated. Lore-id: d7e82b3e Constraint: boundary links point only at the worktree's own packages, never another checkout Constraint: destructive remediation requires positive identity proof, never a fail-closed guess Rejected: empty node_modules boundary | empirically does not stop Node/Bun ancestor walk-up Rejected: reject launches when an ancestor node_modules exists | breaks legitimate nested-repo workflows Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 44-test launch-worktree suite incl. real module-resolution probe and non-deletion regression; harness e2e 13/13; dogfood dist/gjc --worktree resolves own commit pre-install Not-tested: real Windows/macOS case-variant filesystems (CI covers linux)
…lidation Fourth review follow-up to snowykr's review of #4626 (six required changes): 1. Boundary reconciliation: a launcher-owned boundary directory now carries a .gjc-node-modules-boundary marker. Every launch reconciles it against the current commit's workspace members — stale member links from a previous commit are pruned and new members linked — so a reused worktree checked out at a different commit can never resolve deleted members through an ancestor node_modules. 2. Complete workspace discovery with fail-closed semantics: the boundary reads package.json workspaces (array and {packages}) AND pnpm-workspace.yaml (via Bun.YAML). Unreadable or malformed root or member manifests are actionable isolation failures (worktree_workspace_manifest_unreadable / _member_invalid), never silent skips. 3. Ownership: remediation removes only positively-identified launcher-created links (target equal to the source checkout's own node_modules) and marker-owned directories. Source containment alone no longer authorizes deletion. 4. No execution through unproven boundaries: a worktree node_modules link whose resolution fails (EACCES/EPERM/EIO/transient) fails the launch with worktree_node_modules_unverified instead of continuing; the link is preserved for the user. Proven dangling links are removed and replaced with the boundary. A link that resolves outside the source tree is provably user-owned external and stays present. 5. Manifest path/name validation: workspace patterns must be relative without traversal segments (worktree_workspace_pattern_unsafe), member names must match npm package grammar including a Windows-separator check (worktree_workspace_member_name_invalid), and both member manifests and final link paths are verified to stay inside the worktree before any mutation. 6. The external-link EACCES regression now uses a persistent target-aware spy that delegates to the real implementation for unrelated paths and asserts the target was reached before asserting the refusal. Lore-id: 051c42a0 Constraint: only marker-owned directories and positively-identified launcher links are ever removed Constraint: workspace discovery must be complete or the launch fails with an actionable typed error Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 50-test launch-worktree suite (8 new: reconciliation, pnpm-yaml, traversal/name/manifest refusals, user-dir preservation); dogfood with marker + reconciliation Not-tested: real pnpm install replacing the boundary end-to-end
…rations Fifth review follow-up to snowykr's review of #4626 (six required changes): 1. Declaration-driven isolation: the share decision consults the worktree's own workspace declaration before the origin link scan, so a workspace repo is never shared even when its origin tree merely happens to contain no self-links (external hoists, partial installs). An unmarked real directory remains user-owned and untouched. 2. Per-link ownership: the boundary records every launcher-created member link in .gjc-node-modules-links.json. Reconciliation replaces and prunes only recorded links; a package-manager install that replaced the tree leaves no recorded ownership, so its entries are never touched even if the marker survives. A directory marker alone no longer authorizes descendant deletion. 3. Dangling links are never auto-removed: a broken resolution proves unavailability, not ownership, so dangling/unreadable links fail the launch with worktree_node_modules_unverified and are preserved for the user. Only a link positively resolving to the source checkout's own node_modules — the exact link this launcher creates — is remediated. 4. Realpath-aware containment: member manifests and member directories must canonically resolve inside the worktree (symlinked members pointing at another checkout are rejected); link destinations use path containment with a symlinked-parent realpath check. Found and fixed via dogfood: containment for link DESTINATIONS must be path-based because the target legitimately lives in packages/. 5. Complete declaration handling: pnpm-workspace.yaml and package.json workspaces are strictly typed (non-string or non-array shapes are isolation failures), merged deterministically when both exist, and a workspace-to-non-workspace transition reconciles the boundary to empty and drops the marker. 6. Entry-state assertions: boundary tests assert lstat link/dir/missing states instead of Bun.file().exists(), and the external-link EACCES regression uses a persistent target-aware spy with a reached flag. Lore-id: 43ec1f3d Constraint: only recorded launcher links are ever replaced or pruned Constraint: link-destination containment is path-based; member containment is realpath-based Confidence: high Scope-risk: narrow Reversibility: single-commit fix-forward Tested: 53-test launch-worktree suite (3 new: pkg-replaced preservation, declaration-gone reconciliation, symlinked-parent rejection); dogfood across relaunches caught and fixed the reuse containment crash Not-tested: real Windows junction/UNC paths (CI covers linux)
The main repository uses the standard object form of package.json workspaces. Treating that form as malformed made --worktree fail before it could create its local resolution boundary. Lore-id: b1a6f3c8 Constraint: workspace declaration parsing must accept the package-manager form used by the repository root Confidence: high Scope-risk: narrow Reversibility: single-commit Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts; bun --cwd=packages/coding-agent run check
The initial entry said workspace worktrees remain without node_modules, but the finalized isolation design creates a launcher-owned local boundary for declared workspace members. Lore-id: 68e5a2d1 Confidence: high Scope-risk: narrow Reversibility: single-commit Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts
…pm negations Review follow-up for #4620 addressing the three standing majors: - An unmarked real node_modules directory in a workspace worktree is now accepted only after it is proven a complete resolution boundary: every declared member must resolve from it to inside the worktree (missing, outside-resolving, and self-looping entries refuse the launch with remediation steps instead of falling through to an ancestor checkout). - The per-link ownership manifest fails closed: malformed manifests and invalid entry names throw instead of authorizing nothing silently, and a recorded link is replaced or pruned only when its current realpath still matches the recorded target, so package-manager replacements are never deleted on the manifest's say-so. - Workspace member selection honors pnpm-style negated patterns (trailing !pattern removes earlier matches) and resolves duplicate member names deterministically. Lore-id: 4f9a1c2b Constraint: preserve Yeachan Heo authorship through rebase onto current dev Constraint: no deletion of user-owned node_modules content on unproven ownership Rejected: deleting incomplete unmarked directories | destroys user installs Rejected: trusting manifest names without identity proof | deletes pm replacements Confidence: high Scope-risk: narrow Reversibility: trivial Tested: launch-worktree.test.ts 63/63 incl. 9 new regressions Tested: cli-root-flags.test.ts 4/4; sdk-broker-lifecycle-e2e.test.ts 82/82 Tested: bun run check:ts; bun --cwd=packages/coding-agent run check Tested: bun scripts/verify-gjc-state-writers.ts --fail Not-tested: Windows junction semantics (POSIX CI only)
…nk boundary proofs Review follow-up for #4626 resolving the four remaining majors at b6a8d19: - Workspace patterns are now processed strictly in declaration order with re-inclusion: ['packages/*', '!packages/legacy', 'packages/legacy'] selects packages/legacy again, matching pnpm/npm ordered semantics, so a re-included member gets a local link instead of resolving through an ancestor checkout. - Boundary metadata (ownership marker and link manifest) must be a plain regular file whose realpath resolves inside the boundary; a symlink at either name — including a dangling one, which lstat still sees — fails closed instead of letting reconciliation read or write through it to an arbitrary external path. - An existing external node_modules symlink is accepted only when it is a complete resolution boundary for the worktree's declared members; a link into an ancestor workspace that resolves members to foreign live sources is refused. - A non-directory node_modules entry (file, FIFO, socket) in a workspace worktree is refused with remediation instead of being classified as isolation. Also rebases onto current dev 9f29ee5 (contains PR event base 50b032e). Lore-id: 8c2d4e6f Constraint: preserve Yeachan Heo authorship through rebase onto current dev Constraint: never read or write boundary metadata through a symlink Rejected: existsSync absence probe | misses dangling symlinks, would create external targets Rejected: two-phase collect-then-negate pattern matching | breaks ordered re-inclusion semantics Confidence: high Scope-risk: narrow Reversibility: trivial Tested: launch-worktree.test.ts 69/69 incl. 15 new regressions Tested: cli-root-flags.test.ts 4/4 Tested: bun --cwd=packages/coding-agent run check; bun run check:ts (9/9 workspaces) Tested: bun scripts/verify-gjc-state-writers.ts --fail Not-tested: sdk-broker-lifecycle at this head (requeued post-push)
…teness Review follow-up for #4626 resolving the b6a8d19 findings: - Marker-owned member links are installed atomically: the new link is created at a process-unique temporary name in the same directory and rename(2)d over the destination, so the rm-then-symlink crash window that could leave a missing member entry is gone. - Reconciliation ends by re-validating that the boundary resolves every declared member; a crash/ENOSPC-damaged marker-owned partial boundary now fails the launch with worktree_boundary_incomplete_after_reconcile instead of silently resolving through an ancestor checkout. - Test probe uses process.stdout.write instead of console.log. Lore-id: b7e3f1a9 Constraint: preserve Yeachan Heo authorship Constraint: never accept a marker-owned partial boundary as success Rejected: retry loop around rm+symlink | still non-atomic between retries Rejected: completeness check only at accept time | misses damaged reconcile path Confidence: high Scope-risk: narrow Reversibility: trivial Tested: launch-worktree.test.ts 70/70 incl. damaged-replacement regression Tested: cli-root-flags.test.ts 4/4; sdk-broker-lifecycle-e2e.test.ts 83/83 Tested: bun --cwd=packages/coding-agent run check; verify-gjc-state-writers --fail Not-tested: concurrent same-worktree launches (single-user CLI invariant)
… case identity Review follow-up for #4626 resolving the snowykr findings at 31cd3a5: - Workspace declarations are evaluated independently and unioned: a !pattern in package.json can no longer exclude a member that pnpm-workspace.yaml positively selects (per-declaration ordered negation semantics). - Root manifests must be lstat-verified regular files; a dangling or non-regular entry at package.json / pnpm-workspace.yaml is an obstruction, not an absent declaration. Negated pattern bodies are validated after stripping '!'. Selected members without a package name refuse the launch instead of silently skipping. - A proven-safe source dependency tree is preserved across reuse in a plain (non-workspace) repository: the launcher's own link is re-established instead of leaving the worktree without dependencies. - Path-identity case folding is measured per volume (two-entry probe, cached) instead of assumed from the platform, so case-sensitive APFS volumes no longer fold distinct identities. - The self-link scan refuses to traverse a node_modules root symlinked outside the source repo, and fails closed when the depth cap leaves a directory unexamined. Lore-id: c4e8b2d1 Constraint: preserve Yeachan Heo authorship Constraint: fail closed on every unprovable boundary input Rejected: platform-wide case assumption | wrong on case-sensitive APFS Rejected: merging declaration pattern lists | breaks per-file negation semantics Confidence: high Scope-risk: narrow Reversibility: trivial Tested: launch-worktree.test.ts 74/74 incl. mixed-declaration union, nameless member, external root symlink, plain-repo preservation regressions Tested: cli-root-flags.test.ts 4/4; sdk-broker-lifecycle-e2e.test.ts 83/83 Tested: bun --cwd=packages/coding-agent run check; verify-gjc-state-writers --fail Not-tested: real case-sensitive APFS volume (probe logic unit-covered on Linux)
The case-fold capability probe removed fixed checkout-relative names recursively, destroying user entries at .gjc-case-probe-* and racing parallel probes; it now probes inside a unique launcher-owned temp dir. A catch-all tryLstat read EACCES/EIO as absent, routing an unreadable root manifest down the plain-repository sharing path; only ENOENT now means absent. Recursive workspace patterns adopted installed node_modules dependency manifests (and the root manifest) as members; both are excluded. Boundary inspection through reconciliation and metadata commit ran unlocked, letting a concurrent launch accept a half-populated boundary; a stale-breakable per-worktree lock directory now serializes the transaction. Metadata was written by pathname after validation (TOCTOU swap, torn reads); commits are now an exclusive temporary file plus rename. Reconciliation read and wrote a second ownership map that the caller's stale map then overwrote, so A->B->A member changes failed completeness; one map is pruned, recorded, and committed once, and recorded links left dangling are pruned rather than left to shadow resolution. Lore-id: i4620a11 Constraint: launch path stays synchronous (process bootstrap), matching existing sync IO in utils/git.ts and config-file.ts Rejected: async Bun.file/Bun.write conversion | churns 93 sync test call sites and both bootstrap callers for a style minor with no behavior gain Confidence: high Scope-risk: narrow Reversibility: single-commit Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts (80 pass), biome check, tsc --noEmit Not-tested: macOS/Windows case-folding volumes, real ENOSPC injection
…ink scan The self-link scan classified any symlink whose realpath fell under the source root as a workspace self-link. Ordinary `.bin` shims resolve to `<sourceRoot>/node_modules/<pkg>/bin/<tool>.js`, which satisfied that predicate, so a plain non-workspace repo carrying a single `.bin` entry was refused a shared tree and its worktree received no `node_modules` at all -- strictly worse than the pre-fix sharing it replaced. Essentially every real install writes `.bin` entries, so this hit the common case. Links that stay inside the scanned `node_modules` tree describe the install graph, not the repository's sources, and cannot couple two checkouts. Only targets escaping the tree onto repository sources (`node_modules/@scope/pkg -> ../../packages/pkg`) create that coupling, and those are still reported -- including links buried deep in isolated layouts, which the added guard pins. Lore-id: 4b1e9c37 Constraint: exclusion must be scoped to the scanned tree's realpath -- a link escaping node_modules onto packages/ must still refuse sharing Constraint: case-insensitive volumes must fold the containment check, or a differently-cased .bin target re-enters the false-positive path Rejected: skip a literal ".bin" path segment | misses nested dependency links in isolated layouts and is trivially defeated by any other intra-tree link name Rejected: drop the source-root prefix test entirely | would stop detecting genuine workspace self-links, reopening #4620 Confidence: high Scope-risk: narrow Reversibility: easy Tested: plain repo with .bin shim shares and resolves; deep isolated-layout link escaping to packages/ still refuses; both #4620 contamination classes and reverse-mutation safety re-verified at CLI and broker callsites Not-tested: Windows junction reuse against a case-insensitive volume
…xact filesystem reads Seven merge blockers from the exact-head review of the isolation boundary. The unifying defect: several filesystem reads collapsed "unreadable" into "absent", and several writes assumed no other launcher existed. tryRealpath returned null for every failure, and a caller deletes on null -- so an EACCES from a permissions change, or an EIO from a network mount, made a valid link look dangling and destroyed it. Only BROKEN_LINK_CODES now yield null; everything else propagates. The one site that legitimately treats unreadable and dangling alike (preserve the link, refuse the launch) does so through an explicit local helper instead. The source node_modules probe moved off existsSync for the same reason. The lock became an owner-bound lease: each holder writes a unique token and refreshes it, a lease is breakable only when its own token stopped being refreshed, the breaker removes only the exact token it observed, and release verifies the token -- so a timed-out holder can no longer tear down the lock a different launch acquired and run two reconciliations at once. Ownership is now claimed before the first link, making an interrupted setup a detectable partial boundary rather than an unmarked tree of unattributable links; unrecorded links under an existing marker are adopted at their actual on-disk target so a stale member can still be corrected. Stale-link removal re-proves parent containment immediately before deleting, closing the swapped-parent escape. Member discovery follows symlinks, so a declared directory-symlink member no longer vanishes into a silently empty set. Unchanged links are left alone instead of recreated. A node_modules that appears mid-setup is revalidated instead of accepted as complete. The .bin exclusion and the 4-class both-callsite behaviour are unchanged. Lore-id: 7c92f4ab Constraint: null from tryRealpath drives deletion -- it must mean "provably resolves nowhere", never "could not read" Constraint: lease staleness must be measured from the heartbeat, not from acquisition, or a long-running launch is stolen from Constraint: adoption must record the link's current target, not the desired one, or the identity check cannot detect a stale link Rejected: keep the bare-directory lock and lengthen the timeout | a slow launch is still stolen from, and release still removes another's lock Rejected: order manifest before links | the inverse crash window is worse: a manifest claiming links that do not exist Confidence: high Scope-risk: moderate Reversibility: easy Tested: EACCES realpath preserves a valid link; EACCES source node_modules is not read as absent; mid-flight node_modules refused; unrecorded link adopted and corrected; directory-symlink member discovered; unchanged link performs no symlink write; deletion through a swapped outside parent leaves the external file intact; non-object member manifest rejected; abandoned lease broken, heartbeating lease preserved, re-acquired lock not released Not-tested: real Windows junction reuse (platform behaviour asserted via the no-write path, not on a Windows runner)
Synchronous boundary work cannot renew a timer-based lease, and path validation cannot make destructive stale-link cleanup safe under parent swaps. The launcher now reclaims only dead or old ownerless locks, preserves unowned and stale member links, refuses ambiguous marker state, and closes the node_modules creation and internal-symlink scan races. Lore-id: 4626-v5-boundary-races Constraint: never delete a user-owned or unproven node_modules entry Constraint: synchronous launch work must not depend on timer heartbeats Rejected: timer-based lease renewal | Bun synchronous filesystem work cannot service the interval Rejected: stale-link unlink after lexical containment | a swapped parent can redirect the delete Confidence: high Scope-risk: regression Reversibility: easy Tested: launch-worktree.test.ts 90/90; cli-root-flags.test.ts 4/4; coding-agent typecheck; bun run build Not-tested: real Windows junction execution; full broker e2e had unrelated lifecycle-child failures Directive: preserve ambiguous boundary entries and require explicit cleanup rather than guessing ownership
Stale lock reclaim and symlink traversal must never operate on a pathname that another process can retarget. Canonical traversal and empty-directory lock breaking preserve successor ownership while the boundary publication guard fails closed when node_modules is swapped during reconciliation. Lore-id: 4626-v6-canonical-boundary-publication Constraint: never delete or overwrite a successor lock or outside boundary path Constraint: preserve plain-repository reuse and workspace own-commit resolution Rejected: pathname rename-and-remove stale lock reclaim | can move a successor lock Rejected: mutable symlink traversal | retargeted paths can escape the validated tree Confidence: high Scope-risk: regression Reversibility: easy Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts; bun test packages/coding-agent/test/cli-root-flags.test.ts; bun --cwd=packages/coding-agent run check; bun run build; bun scripts/verify-gjc-state-writers.ts --fail Not-tested: real Windows junction execution; full broker e2e has three unrelated lifecycle-child exit failures
Scoped workspace link parents are now created and verified as stable directories, then publication uses their canonical identity instead of a replaceable pathname. A concurrent parent swap therefore fails closed or leaves writes inside the original boundary. Lore-id: 4626-v7-link-parent-identity Constraint: boundary publication must not follow mutable package-manager parent paths Confidence: high Scope-risk: regression Reversibility: easy Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts; bun --cwd=packages/coding-agent run check
Exercise both stable-parent diagnostics so the canonical publication hardening cannot regress behind the older containment guard. Confidence: high Scope-risk: narrow Reversibility: trivial Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts
Prevent successor-lock deletion, bound ownerless recovery, and refuse canonical-path retargets during dependency boundary publication and traversal. Preserve supported physical worktree bucket aliases with deterministic adversarial coverage. Confidence: high Scope-risk: wide Reversibility: easy Tested: bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts; bun test packages/coding-agent/test/cli-root-flags.test.ts; bun --cwd=packages/coding-agent run check; bun scripts/verify-gjc-state-writers.ts --fail; bun run lint; bun run build
|
Current-dev reconciliation update for PR #4626:
All prior-head review and CI evidence is stale. Please review exact current head after fresh CI completes. — |
|
Current exact-head verification update for PR #4626:
— |
Closes #4620
What
Rebased the worktree
node_modulesisolation fix onto the currentdevbase.Workspace worktrees no longer reuse a source-checkout dependency tree that can resolve workspace imports to a sibling checkout or let a worktree install mutate the source installation. The launcher creates a validated local boundary for declared workspace members, preserves user-owned dependency trees, and refuses unproven boundary state rather than guessing ownership or deleting through a mutable path.
The fix covers the reviewed failure classes:
.binand nested dependency links remain shareable;node_modulesexclusion, and symlinked-member containment;node_modulesdirectory appearing during setup is revalidated as a complete local boundary instead of being silently claimed.Verification
Exact base/head:
3cda2e4c99557e3bbf686663f1a9ae5cc0801db9(origin/dev; merge base is exact)a9afa64fad02cf1a70e43e5a8cd5451deb355d88packages/coding-agent/CHANGELOG.md,packages/coding-agent/src/gjc-runtime/launch-worktree.ts,packages/coding-agent/test/gjc-runtime/launch-worktree.test.tssha256:60c8f3a280f0e9d9584ee55761f46e6e5309194aad7711eeabe26e508f87baebLocal evidence on this exact source tree:
bun test packages/coding-agent/test/gjc-runtime/launch-worktree.test.ts— 94 pass / 0 fail / 226 assertionsbun test packages/coding-agent/test/cli-root-flags.test.ts— 4 pass / 0 fail / 49 assertionsbun --cwd=packages/coding-agent run check— pass (Biome and coding-agent TypeScript checks)bun run build— pass; native addon rebuilt and compiled CLI producedbun scripts/verify-gjc-state-writers.ts --fail— passbun run lint— passgit diff --check— passThe broker lifecycle suite was run and exposed three lifecycle-child/cleanup failures (81 pass / 3 fail); no broker source was changed and this PR has no green broker receipt. Windows/Darwin hosted launch coverage is unavailable in this Linux worktree; the implementation uses platform-aware identity probing and the Windows junction reuse path avoids rewriting unchanged links.
Exact-head CI refresh
For head
a9afa64fad02cf1a70e43e5a8cd5451deb355d88against base3cda2e4c99557e3bbf686663f1a9ae5cc0801db9, Dev CI run32564528133passed the affected plan, native build, launch-worktree test, TypeScript build, evidence producer, affected aggregate, virtual integration, and state gates. The only failing check is the intentional PR-contract bootstrap while independent approval is pending.Risk classification
low-riskregression-riskhigh-riskThis changes the synchronous launch path, filesystem boundary ownership, lock recovery, and cleanup refusal semantics. It requires an independent exact-head approval; owner self-approval is intentionally not used.
GJC verdict
Prior review requests target superseded heads. @probepark @snowykr — please review exact head
a9afa64fad02cf1a70e43e5a8cd5451deb355d88; merge remains blocked until an authenticated independent exact-head approval and green required CI are present.—
[repo owner's gaebal-gajae (clawdbot) 🦞]