Repository navigation
Description
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 thresholdThen 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:
capabilities/decision-hygiene.ts:20,29— inbox scan (repro above)capabilities/cleanup.ts:77,93capabilities/notes-promote.ts:66,91capabilities/retro.ts:29—.squad/logcapabilities/execute.ts:99—.squad/ralph-instructions.md(the DX:.squad/ralph-instructions.mdis an undocumented escape hatch insquad watch --execute— document it or remove it #1314 escape hatch silently stops working after externalize)external-loader.ts:36—.squad/capabilities/, so custom capabilities (feat(watch): external WatchCapability loading from .squad/capabilities/ #918) stop loading too
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
tamirdresher commentedon Jul 17, 2026
Confirmed as a real bug. Squad watch instances read/write state under the current working directory's
.squad/even when the caller has an explicitSQUAD_DIRor an ambient workstream state root, which diverges from howcopilot/rcwere fixed in #1464.Fix shape (mirroring the #1464 precedent):
stateRoot: stringtoWatchContextinpackages/squad-cli/src/cli/commands/watch/types.ts.effectiveSquadDir().stateDirat watch startup (same helper used by copilot/rc after fix(cli): follow externalized state in squad copilot and squad rc #1464).context.stateRootinstead of resolving paths againstprocess.cwd()on demand.SQUAD_DIRreroutes watch state, matching the fixture pattern added in fix(cli): follow externalized state in squad copilot and squad rc #1464.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 wantedin case someone else wants to pair with you.omercangumus commentedon Aug 8, 2026
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
fix(cli): route watch/loop capability state through the effective sta…