Skip to content

fix(cli): route watch/loop capability state through the effective state dir - #1660

Merged
bradygaster merged 2 commits into
bradygaster:devfrom
omercangumus:omercangumus/1490-watch-external-state
Aug 23, 2026
Merged

fix(cli): route watch/loop capability state through the effective state dir#1660
bradygaster merged 2 commits into
bradygaster:devfrom
omercangumus:omercangumus/1490-watch-external-state

Conversation

@omercangumus

Copy link
Copy Markdown
Contributor

What

squad watch and squad loop now thread the effective state directory (stateRoot) through WatchContext and every capability, instead of each capability rebuilding .squad/ paths from teamRoot on its own.

Why

Closes #1490

Full repro and root cause are in the issue — the short version: runWatch/runLoop already call effectiveSquadDir() and use the external-aware stateDir for team.md/routing.md, but every capability was still handed plain teamRoot and joined .squad/ onto it directly. After squad externalize, the local .squad/ is a marker-only stub (config.json plus a few other KEEP_LOCAL files), so every capability doing path.join(teamRoot, '.squad', ...) silently found nothing: decision-hygiene's inbox count, cleanup's scratch/log/orchestration-log pruning, retro's staleness check, subsquad discovery, the capabilities/ external loader, and the ralph-instructions.md lookup in execute.ts.

How

  • types.ts: added stateRoot: string to WatchContext.
  • watch/index.ts / loop.ts: set stateRoot: stateDir in baseContextstateDir was already computed via effectiveSquadDir() in both files, just never threaded further. The compiler caught loop.ts's missing field the moment stateRoot became required — same bug, second entry point, would've been an easy miss by hand.
  • decision-hygiene.ts, cleanup.ts, retro.ts: swapped path.join(context.teamRoot, '.squad', ...) for path.join(context.stateRoot, ...).
  • discoverSubSquads() (private in watch/index.ts, only caller was runWatch itself): same swap, now takes stateRoot directly. Exported it (matching the existing reportBoard precedent in the same file) so it's actually testable.
  • loadExternalCapabilities() and buildAgentPrompt(): both took a bare teamRoot and joined .squad/ internally, with existing test suites built around that exact contract (7 tests for the loader, 4 for the prompt builder). Rather than changing what they join, both got a new optional param (capabilitiesDir / stateDir) that wins when passed — zero behavior change for any existing 2-arg caller, callers with the full context now pass context.stateRoot as the extra arg.
  • notes-promote.ts: left unchanged, differs from what the issue originally listed. It only reads .squad/config.json for backend-type detection — config.json is deliberately KEEP_LOCAL in externalize.ts, and the two-layer backend's actual state (git-notes + orphan branch) lives in git refs, not the filesystem, so externalization doesn't touch any of it. Flagging this as a correction to the issue's own root-cause list, not silently dropping scope.

Testing

Regression tests added to the existing suites, all following each file's established mock/fixture style — no new conventions:

  • test/cli/watch-capabilities.test.ts: new externalized state (#1490) blocks under CleanupCapability and DecisionHygieneCapability, a new RetroCapability describe block (didn't exist before), and a buildAgentPrompt override test — each gives teamRoot/stateRoot different fake paths and asserts the mocked storage calls only ever touch stateRoot.
  • test/cli/watch-external-loader.test.ts: new block proving capabilitiesDir wins over the teamRoot-derived default, and that omitting it preserves the old default exactly (existing 7 tests untouched).
  • test/watch-subsquad-discovery.test.ts (new file): real-filesystem test for the now-exported discoverSubSquads, including one that writes a subsquad only under an "external" dir and shows it's invisible from the local .squad/ — the actual shape of the bug.
  • Ran every new/changed test against unfixed dev first (stashed the 8 source files, kept the tests): 12 failures — the exact ones exercising the fix (Windows-path assertions, missing export, override ignored). Restored the source, all pass.
npx vitest run test/cli/watch-capabilities.test.ts test/watch-cleanup.test.ts test/watch-notes-promote.test.ts test/cli/watch-agent-spawn.test.ts test/cli/watch-external-loader.test.ts test/watch-subsquad-discovery.test.ts
 Test Files  1 failed | 5 passed (6)
      Tests  1 failed | 97 passed (98)

The 1 failure (buildAgentPrompt > checks .squad/ralph-instructions.md inside teamRoot) is a pre-existing Windows path-separator mismatch, confirmed present on unmodified dev — untouched by this PR, same function, different (2-arg, unchanged-behavior) call path.

Full-suite (npx vitest run, fresh dist for both packages): 87 failures / 7232 passed. Checked every uniquely-named failing file against unmodified dev: test/cli/watch.test.ts (cold dynamic-import 5s timeout) and test/cli/upgrade.test.ts (casting_policy_version 1.1 vs 1.2, unrelated template drift) both fail identically with my 13 source files stashed — pre-existing, not branch-caused.

npx tsc --noEmit -p packages/squad-cli/tsconfig.json clean. eslint on all 15 changed files: 0 errors, 107 pre-existing no-console/n/no-sync warnings matching each file's existing style, nothing new.

Diff hygiene: git diff vs git diff -w identical on every file (0 lines difference), including the two files that turned out to have mixed CRLF/LF/BOM conventions on this box (cleanup.ts is CRLF+BOM, most of the rest LF) — verified each against git show HEAD:<file> directly rather than the working-tree copy, since this session hit a real discrepancy where several files read back as CRLF on disk despite being LF in the actual git blob.


⚠️ Quick Check

  • Changeset added: .changeset/fix-1490-watch-external-state.md (patch @bradygaster/squad-cli)

PR Readiness Checklist

Branch & Commit

  • Branch created from dev
  • Branch is up to date with dev
  • Verified diff contains only intended changes (8 source + 6 test files + changeset)
  • PR is not in draft mode
  • Commit history is clean (single commit)

Build & Test

  • npm run build -w packages/squad-cli passes
  • npm test — targeted suites 97/98 (1 pre-existing unrelated failure); full-suite baseline compared against unmodified dev
  • npm run lint passes (tsc clean)
  • eslint — 0 errors

Changeset

  • Changeset added (cli patch)

Docs

  • N/A — internal capability wiring, no public CLI surface changed

Exports

  • discoverSubSquads/SubSquad newly exported from watch/index.ts for testability, matching the existing reportBoard pattern in the same file — not a new module, no subpath export change

Breaking Changes

None. WatchContext.stateRoot is a new required field, but the only two places that construct a WatchContext from scratch (runWatch, runLoop) both already had stateDir in scope and now populate it. External capabilities implementing WatchCapability receive a richer context, not a different one.

Waivers

None.

…te dir

Fixes bradygaster#1490

squad watch and squad loop resolve externalized state for team.md/routing.md
startup reads only, then hand every capability teamRoot and let them build
.squad/ paths from it directly. After squad externalize the local .squad/
is a marker-only stub, so decision-hygiene, cleanup, retro, subsquad
discovery, the capabilities/ loader, and the ralph-instructions.md check
all silently saw an empty directory instead of the real state.

Added stateRoot to WatchContext, populated from effectiveSquadDir().stateDir
in both runWatch and runLoop (the compiler caught runLoop's missing field
once stateRoot became required — same gap, different entry point). Every
capability now reads/writes through context.stateRoot instead of joining
.squad/ onto teamRoot. loadExternalCapabilities() and buildAgentPrompt()
got an optional override parameter rather than a signature change, so their
existing {teamRoot}/.squad/... default and every test built against it keep
working unchanged. notes-promote.ts needed no change — it only reads the
always-local config.json for backend-type detection; two-layer's actual
state is git-notes/orphan-branch, not filesystem.

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

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 7f5d5c8

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 3 item(s) to address before review

Status Check Details
Single commit 2 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 7 check(s) still running

Files Changed (15 files, +320 −17)

File +/−
.changeset/fix-1490-watch-external-state.md +7 −0
packages/squad-cli/src/cli/commands/loop.ts +1 −0
packages/squad-cli/src/cli/commands/watch/capabilities/cleanup.ts +2 −3
packages/squad-cli/src/cli/commands/watch/capabilities/decision-hygiene.ts +2 −2
packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts +10 −3
packages/squad-cli/src/cli/commands/watch/capabilities/retro.ts +1 −1
packages/squad-cli/src/cli/commands/watch/external-loader.ts +5 −2
packages/squad-cli/src/cli/commands/watch/index.ts +13 −6
packages/squad-cli/src/cli/commands/watch/types.ts +7 −0
test/cli/watch-agent-spawn.test.ts +1 −0
test/cli/watch-capabilities.test.ts +118 −0
test/cli/watch-external-loader.test.ts +79 −0
test/watch-cleanup.test.ts +1 −0
test/watch-notes-promote.test.ts +1 −0
test/watch-subsquad-discovery.test.ts +72 −0

Total: +320 −17


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 Aug 8, 2026

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #1660

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 15
Files added 2
Files modified 13
Files deleted 0
Modules touched 3
Critical files 1

🎯 Risk Factors

  • 15 files changed (6-20 → MEDIUM)
  • 3 modules touched (2-4 → MEDIUM)
  • Critical files touched: packages/squad-cli/src/cli/commands/watch/index.ts

📦 Modules Affected

root (1 file)
  • .changeset/fix-1490-watch-external-state.md
squad-cli (8 files)
  • packages/squad-cli/src/cli/commands/loop.ts
  • packages/squad-cli/src/cli/commands/watch/capabilities/cleanup.ts
  • packages/squad-cli/src/cli/commands/watch/capabilities/decision-hygiene.ts
  • packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts
  • packages/squad-cli/src/cli/commands/watch/capabilities/retro.ts
  • packages/squad-cli/src/cli/commands/watch/external-loader.ts
  • packages/squad-cli/src/cli/commands/watch/index.ts
  • packages/squad-cli/src/cli/commands/watch/types.ts
tests (6 files)
  • test/cli/watch-agent-spawn.test.ts
  • test/cli/watch-capabilities.test.ts
  • test/cli/watch-external-loader.test.ts
  • test/watch-cleanup.test.ts
  • test/watch-notes-promote.test.ts
  • test/watch-subsquad-discovery.test.ts

⚠️ Critical Files

  • packages/squad-cli/src/cli/commands/watch/index.ts

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

@omercangumus

Copy link
Copy Markdown
Contributor Author

CI's test job failure is unrelated — upgrade.test.ts still hardcodes casting_policy_version: '1.1', #1643 bumped the template to 1.2 earlier today and missed that assertion. Confirmed identical failure with this PR's diff fully stashed against unmodified dev. Filed #1661, one-line fix, not touching it here to keep this PR scoped to #1490.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Friendly nudge — this PR has had no activity for 9 days.

What needs attention:

  • 🔴 1 CI check(s) failing: test. Fix these first.
  • 👀 No approving reviews yet. Request a review from a teammate.
  • ⬇️ 83 commits behind dev. Rebase to pick up latest changes.

If this PR is abandoned, please close it. If it's blocked on something external, leave a comment so the team knows.
This is an automated check that runs on weekdays. It won't nudge the same PR more than once per week.

@bradygaster
bradygaster merged commit 7e48d31 into bradygaster:dev Aug 23, 2026
16 checks passed
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.

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

2 participants