Repository navigation
Conversation
🟠 Impact Analysis — PR #2351Risk tier: 🟠 HIGH 📊 Summary
🎯 Risk Factors
📦 Modules Affecteddocs (1 file)
root (1 file)
scripts (1 file)
squad-sdk (2 files)
tests (3 files)
This report is generated automatically for every PR. See #733 for details. |
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 3 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | 0 active Copilot thread(s) resolved (2 outdated skipped) |
| ❌ | CI passing | 9 check(s) still running |
Files Changed (8 files, +494 −10)
| File | +/− |
|---|---|
.changeset/linked-team-git-backend.md |
+5 −0 |
docs/src/content/docs/features/state-backends.md |
+2 −0 |
packages/squad-sdk/src/resolution.ts |
+93 −5 |
packages/squad-sdk/src/state-backend.ts |
+16 −3 |
scripts/release/test-reconciliation.mjs |
+1 −0 |
test/cli/hook-linked-project.test.ts |
+87 −0 |
test/cli/state-mcp.test.ts |
+67 −2 |
test/linked-team-state-boundary.test.ts |
+223 −0 |
Total: +494 −10
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
|
Thanks Jose, this is the right fix direction and the storage adapter part is solid (roots at teamSquadDir, normalizes Windows separators and case, rejects outside paths and dot segments, keeps the root delete guard). One boundary issue we need closed before merging, found in our security pass on 0ef7eb3: In packages/squad-sdk/src/resolution.ts around line 975, What we need:
Not a question of intent, just a boundary we have to hold since this path decides where state gets written. Happy to review again as soon as it is pushed. |
|
👋 Friendly nudge — this PR has had no activity for 7 days. What needs attention:
If this PR is abandoned, please close it. If it's blocked on something external, leave a comment so the team knows. |
0ef7eb3 to
24e6789
Compare
|
Thanks a lot for the careful security pass, Tamir, and sorry this sat for a while. I missed your review in the middle of the repo move. Good catch: containing the team dir is clearly not the same as being the team repo. Pushed 24e6789, rebased on the new
|
There was a problem hiding this comment.
🟡 Changes recommended
Linked-project hooks can skip reviewer lockout checks, while Windows path casing can cause valid team repositories to be rejected and prevent state tools from starting.
2 open issues
What changed in this PR
Fixes #2320 by keeping linked-team state on the team repository’s Git backend and aligning storage paths with the team’s .squad directory.
Changes:
- Routes linked
orphanandtwo-layerbackends to the team repository. - Adds repository-boundary validation and regression coverage for linked state operations.
- Documents linked-project behavior and adds an SDK patch changeset.
| File | Description |
|---|---|
test/linked-team-state-boundary.test.ts |
Tests repository boundaries, worktrees, links, and path casing. |
test/cli/state-mcp.test.ts |
Tests linked-team state tools and missing-repository errors. |
packages/squad-sdk/src/state-backend.ts |
Extracts backend configuration selection into a helper. |
packages/squad-sdk/src/resolution.ts |
Resolves the team’s Git repository and roots storage at its squad directory. |
docs/src/content/docs/features/state-backends.md |
Documents linked-project storage and repository requirements. |
.changeset/linked-team-git-backend.md |
Records the SDK patch and its behavior changes. |
Review effort: Balanced (auto)
In a linked project on the orphan or two-layer backend, resolveSquadState ran the git backend in the project repo and rooted the storage adapter at the project's .squad, while every squad_state tool builds paths under the team's .squad. Each memory.*, squad_state_write and squad_state_read call failed with "path is outside squadDir". The team repo owns the state: - in linked mode with a git backend, the backend runs in the team repo. Its git top level must be exactly the team repo (a worktree root counts), compared after resolving links, drive letters and case. A team dir that only sits inside another repository, through "..", an absolute teamRoot or a symlink/junction, and a team .squad linked out of the team repo are refused, as is a team root outside any git repository; - only the backend moves: the context's repoRoot stays the project's repository, which the Claude hooks and the backend MCP harnesses use for project paths and agent turns; - the adapter is rooted at the team's .squad, the root the local backend and the tools already use (unchanged outside linked mode); - the config parsing in resolveStateBackend moves to configuredStateBackend() so the type can be checked first. Closes github#2320
24e6789 to
aa2e033
Compare
… reconciliation The two drive-letter/case tests run only on Windows (it.runIf win32), so Linux release evidence reported them as unexplained skips. Following the repo's [platform: win32] convention, they get a title prefix and a reviewed env-gated rule with runsOn: 'win32', which also makes them required, passing executions in Windows evidence.
…ring them in the Windows release job The Windows release job runs a fixed list of test files, so a runsOn rule made these two tests required there and reported them missing. Use the same plain env-gated rule as the other it.runIf(win32) tests (mcp-spec-init) and leave the release workflow untouched.
|
Hi @tamirdresher, a short update since my reply above: Copilot's review found two more things, both fixed and resolved.
The two Windows-only casing tests are registered as env-gated in the release reconciliation, the same way the |

What
A linked project on the
orphanortwo-layerstate backend can usesquad_stateagain. The git backend runs in the team repo, and the storage adapter is rooted at the team's.squad. The team repo's git root has to be the team repo itself: if the team root isn't in a git repository, or only sits inside another one, resolution fails with a message that says so instead of writing state somewhere else.Why
Closes #2320
resolveSquadStateran the git backend in the project repo and rootedStateBackendStorageAdapterat the project's.squad, whilestate-mcproots every tool at the team's.squad. Everymemory.*,squad_state_writeandsquad_state_readcall therefore failed with "path is outside squadDir". As Tamir confirmed on the issue, the team repo owns.squad/, so its state belongs on the team repo'ssquad-statebranch.How
resolution.ts,resolveSquadState.In remote (linked) mode with a configured
orphanortwo-layerbackend, the state backend runs in the team repo, picked by a newlinkedTeamRepoRoot():teamRootwhen it holds.squad/(whatsquad linkwrites), its parent whenteamRootnames the.squad/dir, orteamRootwhen the squad files sit at its root.git rev-parse --show-toplevelmust equal the team repo, not just contain it. A linked worktree's root counts.canonicalDir()(realpath) andsamePath(). So.., absolute paths, symlinks or junctions, 8.3 names and drive-letter or case differences don't change the answer.It fails closed, with a message naming the team root and the project's
config.json, when:.squadis a link out of the team repo;Before this PR, every state call failed instead. In the first version of this PR, a team dir with no repo of its own resolved to the enclosing repo.
A
teamRootthat is itself a link to a team repo of its own is accepted. That repo's root is exactly what the link points at, so there's no other repository involved.The adapter is rooted at
paths.teamSquadDir, the same root the local backend (FSStorageProvider) and every tool already use. Outside linked modeteamSquadDirequalsprojectDir, so nothing changes there.Only the backend moves.
repoRootin the returned context stays the project's (worktree-aware) repository, because callers use it for project paths: the Claude hook's reviewer-lockout check (hook.ts) and the backend MCP harnesses' agent turns (backend-mcp.ts). The first version of this PR changedrepoRoottoo, which would have broken both; Copilot's review caught the hook.On Windows, a
teamRootthat names the squad dir with different casing (C:\team\.SQUAD) is recognised as the squad dir, so the team repo is its parent.state-backend.ts. The config parsing inresolveStateBackendmoves intoconfiguredStateBackend(), soresolveSquadStatecan check the type before creating the backend. It's not exported from the package index, andresolveStateBackendbehaves the same.What stays the same:
config.json. I left that alone on purpose; there's a related question about it below.There's no "trusted link record" escape hatch: Squad has no such record today, so anything that doesn't resolve to the team repo's own root is refused. If you want one later, it can go on top of this.
Related question, not changed here:
squad linkwrites onlyteamRootandprojectKeyto the project'sconfig.json, neverstateBackend. If the team repo itself runs onorphan(set in the team's ownconfig.json), a project linked withsquad linktherefore resolves tolocaland writes straight into the team repo's working tree instead of itssquad-statebranch. Should a linked project take the team'sstateBackendwhen its own config doesn't set one? I can open a separate issue for that.Testing
New tests in
test/cli/state-mcp.test.ts, through a realcreateStateMcpSessionfrom the project dir:orphanand withtwo-layer:squad_state_write,squad_state_readandmemory.writesucceed;refs/heads/squad-state(fororphan, the written key is on it);squad-statebranch and no notes;orphanwhose team root isn't in a git repository:resolveSquadStatethrows the message above. This one runs under the system temp dir, since anywhere inside the checkoutgit rev-parsefinds the Squad repo itself.New
test/linked-team-state-boundary.test.ts(20 tests, all under the system temp dir, for the boundary from the review):..teamRootvalues that land on the team repo;teamRootthat names the team's.squaddir;teamRootsymlink or junction to a team repo of its own;teamRootspelled with a lowercase drive letter and an uppercased path, both for the team repo and for its.squaddir (spelled.SQUAD).repoRootstays the project's repository while the backend runs in the team repo (orphanandtwo-layer).squad-statebranch or notes created in any repo:..-traversalteamRoot, onorphanand ontwo-layer;teamRootsymlink or junction to such a dir;.squadthat is a symlink or junction into another repository;deleteSync,deleteDirSync,writeSynconteamSquadDir).New
test/cli/hook-linked-project.test.ts: the realsquad hook claude PreToolUsecommand in a linked project onorphanand ontwo-layerstill warns a locked-out member who edits a project file covered by a lockout.Results:
distrebuilt from it (the CLI imports the SDK fromdist), all 3state-mcptests fail. With the change, they pass.repoRooton both backends,.SQUAD) fail against 24e6789 with the SDKdistrebuilt from it, and pass with the fix.state-mcp,state-backend(+ root guard, handshake),resolution,memory-store-resolution,backend,claude-hooksandclaude-v2-review-fixes: 361 passed, 7 skipped, 3 failed. The 3 failures are Windows-only problems in the Claude hook suites, which callrunClaudeHookdirectly and never go through resolution: creating a symlink without permission (EPERM), a folder name containing<and>, and a spawned child process. Separately,state-backend-create-if-absentfails 3 tests the same way as before. The only failures are 3 instate-backend-create-if-absent, which spawns a child with ac:\path as an ESM URL (ERR_UNSUPPORTED_ESM_URL_SCHEME), a Windows-only problem in that test. It doesn't go through resolution.npm run lint: clean. ESLint has no errors, and the only warnings on the new lines aren/no-sync, which applies to the whole file: resolution is synchronous. markdownlint and cspell are clean on the docs and changeset..changeset/linked-team-git-backend.md, sdk patch)PR Readiness Checklist
Branch & Commit
dev(notmain)dev(675c9e9, the newgithub/squadhome)Build & Test
npm run lintpassesDocs
features/state-backends.md: a short "Linked Projects" note (state stays with the team repo, whose Git root must be the team repo itself, and the error otherwise)Breaking Changes
None for working setups. A linked project on a git backend whose team root isn't the root of its own git repository now gets a clear error when it starts. Before, it either failed on every state call or, if the team dir sat inside another repository, used that repository.
Waivers
None.