Skip to content

Two-layer upgrade recreates casting state in working tree while hooks forbid committing it #2172

Open

Description

@Doomblaster

Bug: two-layer squad upgrade recreates .squad/casting/ state in the working tree, while the installed hooks treat it as orphan-branch-only state

When using the two-layer state backend, squad upgrade --state-backend two-layer installs the Git hooks correctly, but it also recreates the following files in the normal working tree:

.squad/casting/history.json
.squad/casting/policy.json
.squad/casting/registry.json

This appears inconsistent with the pre-commit hook installed by Squad.

Installed pre-commit behavior

The generated pre-commit hook contains:

# WI-1: Guard against accidentally committing two-layer mutable state into the
# working tree. If the user has staged any .squad/ paths that are owned by the
# two-layer/orphan backend (decisions.md, agents/*/history.md, casting/, routing/),
# warn and abort so the state stays on the squad-state orphan branch.

if [ -z "$SQUAD_SYNC_ACTIVE" ]; then
  STAGED=$(git diff --cached --name-only 2>/dev/null | grep -E '^\.squad/(decisions\.md|agents/.+/history\.md|casting/|routing/)' || true)

  if [ -n "$STAGED" ]; then
    echo "⚠ squad pre-commit: refusing to commit two-layer state into the working tree." >&2
    echo "  These paths belong on the 'squad-state' orphan branch, not in your normal commits:" >&2
    echo "$STAGED" | sed 's/^/    /' >&2
    echo "  Use 'git restore --staged <path>' to unstage, or set SQUAD_SYNC_ACTIVE=1 to bypass." >&2
    exit 1
  fi
fi

The hook therefore explicitly considers all of .squad/casting/ to belong on the squad-state orphan branch, not in normal commits.

However, immediately after running:

squad upgrade --state-backend two-layer

the upgrade process recreates:

.squad/casting/history.json
.squad/casting/policy.json
.squad/casting/registry.json

in the normal working tree.

This leaves the repository in a confusing state where Squad itself creates files that its own hook says should not exist as normal working-tree state.

There is a second inconsistency in squad.agent.md

The current .squad-templates/squad.agent.md also explicitly lists these files as cast sources:

## Cast sources

- `.squad/casting/registry.json`
- `.squad/casting/history.json`
- `.squad/casting/policy.json`

The current source-of-truth documentation similarly identifies these paths as authoritative/derived casting state.

This raises an important question for the two-layer backend:

Where is an agent supposed to read these files from?

If .squad/casting/* is mutable two-layer state and belongs only on the squad-state orphan branch, those files should normally not be present in the checked-out working tree.

But the generated agent instructions currently point directly to working-tree paths such as:

.squad/casting/registry.json

That seems incompatible with the storage semantics enforced by the hook.

Steps to reproduce

  1. Clone an existing Squad repository configured to use the two-layer backend.
  2. Ensure the existing squad-state state is available.
  3. Run:
squad upgrade --state-backend two-layer
  1. Observe that Squad installs the expected Git hooks.
  2. Observe that Squad also creates:
.squad/casting/history.json
.squad/casting/policy.json
.squad/casting/registry.json

in the working tree.

  1. Stage one of those files.
  2. Attempt to commit.

The Squad pre-commit hook refuses the commit because .squad/casting/ is classified as two-layer state belonging on squad-state.

Expected behavior

The two-layer model should have one consistent source of truth.

If casting/ is mutable state owned by squad-state, then:

  • squad upgrade --state-backend two-layer should not recreate the runtime casting files in the normal working tree.
  • State should be read/written through the two-layer StateBackend.
  • The generated squad.agent.md should not instruct agents to directly use working-tree paths that are intentionally absent.
  • Agent/coordinator access to casting state should go through the appropriate state abstraction or materialization mechanism.

Alternatively, if these three casting JSON files are intentionally supposed to exist in the normal working tree, then the pre-commit hook is too broad:

^\.squad/(...|casting/|...)

and needs to distinguish between working-tree configuration and mutable orphan-branch state.

Why this matters

This is particularly noticeable when onboarding another team member.

squad upgrade --state-backend two-layer is useful because it correctly installs the local Git hooks that are not transferred by git clone.

However, running the upgrade also recreates mutable casting files locally. The developer is then left with files that:

  1. Squad itself created,
  2. the current agent instructions reference,
  3. but Squad's own pre-commit hook says must not be committed to the working tree.

It is therefore unclear whether the correct action is to delete them, leave them untracked, or retain them as local materialized state.

For a shared two-layer Squad repository, that ambiguity can easily result in different developers having different casting state in their working trees.

Related documentation/code

The current repository appears to acknowledge part of this problem already.

The state-backend documentation notes that files can reappear if code writes directly to disk rather than through StateBackend:

A tool, editor save, or agent code path that writes directly via fs.writeFile (bypassing StateBackend) will recreate the file on disk.

The upgrade path itself appears to be causing effectively the same situation for the casting files.

Relevant locations in the current dev branch include:

packages/squad-cli/src/cli/commands/install-hooks.ts
docs/src/content/docs/features/state-backends.md
.squad-templates/squad.agent.md
.squad-templates/casting-reference.md
templates/skills/coordinator-source-of-truth/SKILL.md

Suggested resolution

I think the intended model should be clarified first:

If .squad/casting/* is two-layer mutable state

Then preferably:

  1. Do not create casting/history.json, casting/registry.json, or runtime casting/policy.json in the working tree when the backend is two-layer or orphan.
  2. Ensure upgrade/init operations use StateBackend rather than writing these files directly.
  3. Update squad.agent.md so agents do not depend on directly reading these working-tree paths.
  4. Keep the existing pre-commit protection.

If some casting files are actually working-tree configuration

Then:

  1. Clearly separate configuration from runtime state.
  2. Adjust the pre-commit hook so it does not reject the configuration files.
  3. Document which casting files are committed and which belong on squad-state.

At the moment the installed hook, upgrade behavior, and generated agent instructions appear to describe three different ownership models for the same files.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    wave1:P2Wave 1 PR group P2wave:1-nextWave 1 — immediate next sprint

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions