Skip to content

fix(cli): prevent external state key collisions - #2283

Merged
tamirdresher merged 4 commits into
github:devfrom
omercangumus:omercangumus/1483-external-state-collision
Oct 24, 2026
Merged

tamirdresher merged 4 commits into
github:devfrom
omercangumus:omercangumus/1483-external-state-collision

Conversation

@omercangumus

@omercangumus omercangumus commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

Prevent squad externalize from moving state into a directory owned by another project.

Why

Two different project directories can derive the same project key, causing silent state overwrite. Closes #1483.

How

  • Write an exclusive .owner.json marker before moving files and refuse when the marker names another project.
  • Refuse occupied legacy directories unless the local config already identifies this project as their owner; preserve adoption of that legacy state.
  • Keep the ownership marker out of internalized project files and document the collision behavior and --key remedy.

Validation

Local checks passed: npm run build, npm run lint, npm run lint:eslint (0 errors), npm test -- test/external-state.test.ts (22 tests), markdownlint, and cspell.

The full local npm test run did not complete in this environment: it surfaced missing jq, sandbox-denied writes to AppData\\Roaming, and Vitest 4.1.9 where the repo requires Vitest 5.x. A clean-base comparison reproduced all 12 AppData\\Roaming EPERM failures in test/personal-squad-init.test.ts.

For the one-commit head b5bf63d60c630e10bbd93577f36d60fa25552af5, GitHub Actions test and all applicable checks passed (Squad CI run 39855738598). Its file tree is identical to the previously tested two-commit head. Scope Boundary was skipped as expected: it only runs for repo-health/ branches or PRs carrying the repo-health label; neither applies here. No merge or auto-merge is enabled.


⚠️ Quick Check

  • Changeset added for @bradygaster/squad-cli

PR Readiness Checklist

Branch & Commit

  • Branch created from current dev commit fb15fd5
  • Branch is up to date with dev
  • Diff contains only intended changes (4 files)
  • PR is not in draft mode
  • Commit history is clean (one focused commit)

Build & Test

  • npm run build passes
  • npm test passes on GitHub Actions; local full run was blocked by the environment noted above
  • npm run lint passes
  • npm run lint:eslint passes (0 errors; existing warnings remain)

Changeset

  • Changeset added via .changeset/1483-external-state-collision.md

Docs

  • README section not applicable; this fixes an existing command
  • External State feature page updated

Exports

  • Not applicable; no SDK module or exports changed

Breaking Changes

None.

Waivers

None requested.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 3fe9b20

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 3 item(s) to address before review

Status Check Details
❌ Single commit 4 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 No Copilot review threads
❌ CI passing 12 check(s) still running

Files Changed (4 files, +149 −19)

File +/−
.changeset/1483-external-state-collision.md +5 −0
docs/src/content/docs/features/external-state.md +11 −3
packages/squad-cli/src/cli/commands/externalize.ts +88 −16
test/external-state.test.ts +45 −0

Total: +149 −19


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #2283

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 4
Files added 1
Files modified 3
Files deleted 0
Modules touched 4

🎯 Risk Factors

  • 4 files changed (≤5 → LOW)
  • 4 modules touched (2-4 → MEDIUM)

📦 Modules Affected

docs (1 file)
  • docs/src/content/docs/features/external-state.md
root (1 file)
  • .changeset/1483-external-state-collision.md
squad-cli (1 file)
  • packages/squad-cli/src/cli/commands/externalize.ts
tests (1 file)
  • test/external-state.test.ts

This report is generated automatically for every PR. See #733 for details.

@omercangumus
omercangumus marked this pull request as ready for review October 22, 2026 20:55
Closes github#1483

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@omercangumus
omercangumus force-pushed the omercangumus/1483-external-state-collision branch from bcfe3a3 to b5bf63d Compare October 22, 2026 21:14
@tamirdresher
tamirdresher merged commit 56173f1 into github:dev Oct 24, 2026
21 of 22 checks passed
tamirdresher added a commit that referenced this pull request Oct 24, 2026
squad-release-verify.yml ran policy-gates.mjs --mode=release on pull_request
too, so every PR carrying a changeset failed the gate (#2283, #2301). The
workflow now passes --mode=ci on pull_request and --mode=release only on the
release path (workflow_call from squad-release.yml). In ci mode the gate still
runs: pending fragments are allowed but must be well-formed (allowlisted
package, patch|minor|major bump, non-empty summary); malformed ones fail.
Release mode keeps enforcing the drain unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9
tamirdresher added a commit that referenced this pull request Oct 24, 2026
* fix(cli): 2.0.0-preview.2 upgrade/health fixes from dogfooding

- upgrade --self: honor --dry-run (zero exec), follow the installed channel
  (prerelease -> @insider, stable -> @latest), refuse downgrades without --force (#2295)
- upgrade: one content-compared plan for every squad-owned file; verified
  <file>.local-backup before overwrite; shipped-files manifest distinguishes user
  edits from template revisions; dry-run writes nothing and reports nothing
  right after an upgrade (also on CRLF checkouts); .gitignore/.gitattributes
  appended in the file's own EOL; retired files removed when unmodified;
  provably Squad-shipped stale .copilot/skills copies cleaned (#2297, #2266,
  #2047, #2232)
- health: table headers/placeholders never parsed as agents, built-ins are
  valid routing targets, BOM/pipe-joined legacy charters parse, missing
  identity is a warn with a hint, roster drift warns; upgrade adds built-in
  rows to team.md (#2298, #2234, #1785)
- Rai -> rai: two-step rename recorded with git mv; templates reference
  agents/rai/; init scaffolds rai/ (#2299)
- squad client list|add, dry-run header shows project name, update-check
  stale cache, squad_decide workstream inbox + 80-char slug, doctor Mode: local
  for same-project teamRoot and no DEP0190 (#2299, #2174)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9

* fix(release): make the changeset-drain policy gate event-aware

squad-release-verify.yml ran policy-gates.mjs --mode=release on pull_request
too, so every PR carrying a changeset failed the gate (#2283, #2301). The
workflow now passes --mode=ci on pull_request and --mode=release only on the
release path (workflow_call from squad-release.yml). In ci mode the gate still
runs: pending fragments are allowed but must be well-formed (allowlisted
package, patch|minor|major bump, non-empty summary); malformed ones fail.
Release mode keeps enforcing the drain unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9

* test(cli): align legacy upgrade/help tests with content-compared upgrade and lowercase rai init

- upgrade.test: customized-skill expectations now assert the .local-backup message;
  Rai migration test recreates the legacy mixed-case layout (init scaffolds rai/)
- command-help.test: 'client' has a dedicated help block
- cli-entry: --self --dry-run returns through the single success branch (performed=false)
- team-md: never invent a roster table in a team.md without one
- .github/agents/squad.agent.md: agents/rai/ (template-sync parity)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9

* fix(cli): make optional built-ins explicit

Keep team.md byte-stable during upgrade, report unconfigured optional built-ins as informational, and add a provenance-backed roster command with exact restore semantics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9

* fix(upgrade): preserve compound text templates

Treat compound .example templates as text for EOL-normalized planning and distinguish unproven first-upgrade provenance from confirmed customization in backup output.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9

---------

Co-authored-by: B'Elanna (Squad) <tamirdresher@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 995eea17-c356-4912-90ea-5e5b144e7ba9
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.

squad externalize: project-key collision silently overwrites the external state of another project

2 participants