Skip to content

bug: squad watch capabilities read and write the local .squad/ on externalized projects — silent no-ops #1490

Closed

Description

@omercangumus

Summary

squad watch resolves externalized state for its startup reads only. watch/index.ts calls effectiveSquadDir() and uses the external-aware stateDir to find team.md / routing.md — but then hands every capability teamRoot = path.dirname(local .squad), and the capabilities all build path.join(context.teamRoot, '.squad', ...). On an externalized project the local .squad/ contains only config.json, so watch starts cleanly and then every state-reading capability silently no-ops against an empty directory while the real state sits in the external dir.

This is the watch-shaped remainder of the #1402 family: export (#1396), copilot (#1397), rc (#1398), and cast (#1399) got fixed; #949 fixed watch's startup reads but the capability layer was never in scope.

Steps to reproduce

Verified on dev @ e57b904 (built dist, squad-cli 0.11.0), Windows 11, Node v22.21.0, sandboxed APPDATA.

mkdir -p watchproj/.squad/decisions/inbox && cd watchproj
echo "# Team" > .squad/team.md
echo "# Routing" > .squad/routing.md
for i in 1 2 3 4 5 6; do echo "# decision $i" > ".squad/decisions/inbox/d$i.md"; done

squad externalize
# ✅ Externalized 3 items.

ls .squad/                                          # config.json          ← local is empty
ls <external>/projects/watchproj/decisions/inbox/   # d1.md ... d6.md      ← 6 files, over the >5 threshold

Then resolve the watch context exactly as watch/index.ts:678-681 does and run the decision-hygiene preflight (compiled dist modules, no mocks):

stateDir (startup reads, external-aware): ...\appdata\squad\projects\watchproj
teamRoot handed to capabilities        : ...\watchproj
decision-hygiene preflight: {"ok":false,"reason":"no decision inbox directory found"}

Six inbox files over the merge threshold, and the capability disables itself. Before externalize the same preflight passes.

Root cause

packages/squad-cli/src/cli/commands/watch/index.ts:678-681 — stateDir is used for team.md/routing.md only; teamRoot is derived from the local .squad/ and passed into WatchContext. Every consumer joins .squad/ from it:

watch/config.ts:71 (config.json) is correct as-is — config.json is deliberately KEEP_LOCAL. pid-tracker.ts:22 (.watch-pids) is arguably fine local since it's machine-scoped.

Beyond the silent no-ops there's a split-brain risk: capabilities that trigger writes (decision-hygiene and notes-promote spawn an agent with cwd = teamRoot and prompts that reference .squad/decisions...) would write into the local .squad/ of an externalized project, and a later squad internalize copies the external files over the local ones. Read-side no-op is what's verified above; the write side is the same paths by construction.

Suggested fix direction

Add the effective state dir to WatchContext (e.g. context.stateRoot, set from effectiveSquadDir().stateDir) and have capabilities join state paths from it, keeping teamRoot for repo-scoped operations (git, gh, spawn cwd). Same shape as the copilot/rc fix in #1464.

Can pick this up if useful.

Activity

  1. tamirdresher commented on Jul 17, 2026

    @tamirdresher
    Collaborator

    Confirmed as a real bug. Squad watch instances read/write state under the current working directory's .squad/ even when the caller has an explicit SQUAD_DIR or an ambient workstream state root, which diverges from how copilot / rc were fixed in #1464.

    Fix shape (mirroring the #1464 precedent):

    Since you already scoped this in the issue and offered to pick it up, please go ahead — this one is yours if you want it. Happy to review a draft PR whenever you have one. Marking help wanted in case someone else wants to pair with you.

  2. omercangumus commented on Aug 8, 2026

    @omercangumus
    ContributorAuthor

    picking this up per the fix shape above. going with stateRoot on WatchContext, threaded through decision-hygiene/cleanup/retro. notes-promote.ts turns out fine as-is on closer look — it only reads .squad/config.json (deliberately local) for backend-type detection, the actual two-layer state is git-notes/orphan-branch, not filesystem, so externalization doesn't touch it. capabilities/ loader and ralph-instructions.md check get an optional override param instead of changing their existing (well-tested) signatures. PR shortly

  3. added a commit that references this issue on Aug 23, 2026
    7e48d31
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions