Skip to content

fix(state-backend): conditional .gitignore for two-layer/orphan state files#1229

Open
tamirdresher wants to merge 1 commit into
devfrom
squad/1228-conditional-gitignore-two-layer
Open

fix(state-backend): conditional .gitignore for two-layer/orphan state files#1229
tamirdresher wants to merge 1 commit into
devfrom
squad/1228-conditional-gitignore-two-layer

Conversation

@tamirdresher

Copy link
Copy Markdown
Collaborator

Closes #1228

Summary

Defense-in-depth complement to the pre-commit hook installed by squad upgrade --state-backend two-layer|orphan. Adds .squad/decisions.md and .squad/agents/*/history.md to .gitignore (in a marker-delimited block) when those backends are active, and removes the block when switching back to local.

Result: git add ., git add -A, IDE "stage all", and git commit -am will no longer stage two-layer state into the working tree. The pre-commit hook stays as a hard backstop for git add --force and edge cases.

What changed

  • New helper packages/squad-sdk/src/config/gitignore-state.ts -- addSquadStateGitignoreBlock() and removeSquadStateGitignoreBlock() with marker-delimited block (# Squad: state owned by squad-state branch to # /Squad: ...)
  • packages/squad-sdk/src/config/init.ts -- adds stateBackend to InitOptions; calls helpers based on options.stateBackend
  • packages/squad-cli/src/cli/core/init.ts -- passes stateBackend to sdkInitSquad()
  • packages/squad-cli/src/cli/commands/migrate-backend.ts -- calls helpers on backend transitions (local to orphan/two-layer adds; orphan/two-layer to local removes)
  • Unit tests test/gitignore-state.test.ts -- 14 tests covering idempotent add, idempotent remove, round-trip byte equality, content preservation
  • Integration tests in test/init-scaffolding.test.ts and test/upgrade-state-backend.test.ts
  • Changeset .changeset/conditional-state-gitignore.md (patch bump for both SDK and CLI)

State transitions tested

From To Action
init local local no-op (preserved)
init two-layer/orphan (target) add block
migrate local to orphan/two-layer (target) add block
migrate orphan to two-layer (target) no-op
migrate orphan/two-layer to local local remove block

Test plan

  • npm test on targeted files passes (50 tests: 14 unit + 24 init-scaffolding + 12 migration)
  • npm run build passes
  • Pre-existing failures confirmed to be unrelated (storage-provider timeouts, scheduler, etc. all fail on dev baseline too)

Out of scope

…an backends

Adds .squad/decisions.md and .squad/agents/*/history.md to .gitignore (within a
marker-delimited block) when stateBackend is 'two-layer' or 'orphan'. Removes
the block when switching back to 'local'. Defense-in-depth complement to the
pre-commit hook installed by squad upgrade --state-backend.

- New helper module: packages/squad-sdk/src/config/gitignore-state.ts
  with addSquadStateGitignoreBlock + removeSquadStateGitignoreBlock
- initSquad calls helpers based on options.stateBackend (adds stateBackend
  to InitOptions in SDK)
- CLI runInit passes stateBackend to sdkInitSquad
- migrate-backend calls helpers on backend transitions
- Unit tests for helpers (add/remove idempotency, round-trip)
- Integration tests for init and migration paths

Closes #1228

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 8, 2026 11:09
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 1ec2400

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 2 item(s) to address before review

Status Check Details
Single commit 1 commit — clean history
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 7 check(s) still running

Files Changed (9 files, +505 −1)

File +/−
.changeset/conditional-state-gitignore.md +10 −0
packages/squad-cli/src/cli/commands/migrate-backend.ts +23 −0
packages/squad-cli/src/cli/core/init.ts +1 −0
packages/squad-sdk/src/config/gitignore-state.ts +95 −0
packages/squad-sdk/src/config/index.ts +1 −0
packages/squad-sdk/src/config/init.ts +20 −1
test/gitignore-state.test.ts +178 −0
test/init-scaffolding.test.ts +70 −0
test/upgrade-state-backend.test.ts +107 −0

Total: +505 −1


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

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #1229

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 9
Files added 3
Files modified 6
Files deleted 0
Modules touched 4
Critical files 1

🎯 Risk Factors

  • 9 files changed (6-20 → MEDIUM)
  • 4 modules touched (2-4 → MEDIUM)
  • Critical files touched: packages/squad-sdk/src/config/index.ts

📦 Modules Affected

root (1 file)
  • .changeset/conditional-state-gitignore.md
squad-cli (2 files)
  • packages/squad-cli/src/cli/commands/migrate-backend.ts
  • packages/squad-cli/src/cli/core/init.ts
squad-sdk (3 files)
  • packages/squad-sdk/src/config/gitignore-state.ts
  • packages/squad-sdk/src/config/index.ts
  • packages/squad-sdk/src/config/init.ts
tests (3 files)
  • test/gitignore-state.test.ts
  • test/init-scaffolding.test.ts
  • test/upgrade-state-backend.test.ts

⚠️ Critical Files

  • packages/squad-sdk/src/config/index.ts

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

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🏗️ Architectural Review

⚠️ Architectural review: 1 warning(s).

Severity Category Finding Files
🟡 warning bootstrap-area 1 file(s) in the bootstrap area (packages/squad-cli/src/cli/core/) were modified. These files must maintain zero external dependencies. Review carefully. packages/squad-cli/src/cli/core/init.ts

Automated architectural review — informational only.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds defense-in-depth behavior for the two-layer and orphan state backends by automatically managing a marker-delimited block in .gitignore to prevent accidental staging of squad-state-owned files (.squad/decisions.md and .squad/agents/*/history.md) during init and backend migration.

Changes:

  • Added SDK helpers to add/remove a marker-delimited .gitignore block for squad-state-owned files.
  • Wired the helper into SDK initSquad() and CLI backend migration so the block is added/removed on relevant backend transitions.
  • Added unit + integration tests covering add/remove/idempotency and init/upgrade flows.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/squad-sdk/src/config/gitignore-state.ts Introduces add/remove helpers and marker constants for managing the .gitignore block.
packages/squad-sdk/src/config/init.ts Extends InitOptions with stateBackend and applies the helper during init based on backend choice.
packages/squad-sdk/src/config/index.ts Exports the new gitignore-state helper module from the SDK config barrel.
packages/squad-cli/src/cli/core/init.ts Passes stateBackend through from CLI init to the SDK initializer.
packages/squad-cli/src/cli/commands/migrate-backend.ts Adds logic to update .gitignore marker block when migrating between local/worktree and orphan/two-layer backends.
test/gitignore-state.test.ts New unit tests for idempotent add/remove and round-trip behavior using in-memory storage.
test/init-scaffolding.test.ts Integration tests for initSquad() writing (or not writing) the marker block depending on stateBackend.
test/upgrade-state-backend.test.ts Integration tests for backend migration adding/removing/not-duplicating the .gitignore marker block.
.changeset/conditional-state-gitignore.md Adds a patch changeset for both SDK and CLI to ship the behavior.

Comment on lines +4 to +6
* When the state backend is 'two-layer' or 'orphan', .squad/decisions.md
* and .squad/agents/history.md are owned by the squad-state orphan branch.
* Adding them to .gitignore prevents accidental staging via git add .,
Comment on lines +54 to +56
* Finds the opening and closing marker lines and removes all lines between
* them (inclusive), plus one leading blank line in what follows if present.
* Returns true if the block was removed, false if it was not present.
Comment on lines +158 to +160
* When 'orphan' or 'two-layer', adds a marker-delimited block to .gitignore
* so .squad/decisions.md and .squad/agents/*\/history.md are not accidentally
* staged into the working-tree commit graph.
Comment on lines +243 to +259
it('.gitignore round-trip: local→two-layer→local leaves file byte-identical', { timeout: 30_000 }, async () => {
dir = mkRepo('local');
const gitignorePath = path.join(dir, '.gitignore');
const existingContent = '# my project\nnode_modules/\ndist/\n';
fs.writeFileSync(gitignorePath, existingContent);

await migrateStateBackend(dir, 'two-layer');
// Migrate back to local but this time need to create fresh repo with same config
// We need to start a new repo already set to two-layer then go back
const twoLayerDir = mkRepo('two-layer');
const twoLayerGitignorePath = path.join(twoLayerDir, '.gitignore');
fs.writeFileSync(twoLayerGitignorePath, existingContent);
await migrateStateBackend(twoLayerDir, 'local');
// After round-trip: the marker block should not be present
const finalContent = fs.readFileSync(twoLayerGitignorePath, 'utf-8');
expect(finalContent).not.toContain('# Squad: state owned by squad-state branch');
try { fs.rmSync(twoLayerDir, { recursive: true, force: true }); } catch { /* best-effort */ }
Comment on lines +66 to +82
const content = storage.readSync(gitignorePath) ?? '';
if (!content.includes(SQUAD_STATE_GITIGNORE_OPEN_MARKER)) {
return false;
}

const lines = content.split('\n');
const openIdx = lines.findIndex((l) => l === SQUAD_STATE_GITIGNORE_OPEN_MARKER);
if (openIdx === -1) return false;

// Find the closing marker starting from the opening marker
let closeIdx = -1;
for (let i = openIdx + 1; i < lines.length; i++) {
if (lines[i] === SQUAD_STATE_GITIGNORE_CLOSE_MARKER) {
closeIdx = i;
break;
}
}
Comment on lines +1251 to +1262
// Conditionally add/remove squad-state .gitignore block
// When stateBackend is 'orphan' or 'two-layer', .squad/decisions.md and
// .squad/agents/*/history.md are owned by the squad-state branch — add a
// marker-delimited block so `git add .` cannot accidentally stage them.
// When stateBackend is 'local' (or undefined), remove the block if present.
// -------------------------------------------------------------------------

if (options.stateBackend === 'orphan' || options.stateBackend === 'two-layer') {
addSquadStateGitignoreBlock(gitignorePath, storage);
} else {
removeSquadStateGitignoreBlock(gitignorePath, storage);
}
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.

feat: conditional .gitignore for two-layer/orphan state files (defense-in-depth)

3 participants