diff --git a/.ai-team/decisions.md b/.ai-team/decisions.md index 4a2d6616f..90c0e8926 100644 --- a/.ai-team/decisions.md +++ b/.ai-team/decisions.md @@ -4380,7 +4380,7 @@ The merge base is `d405871`. Since then: - **main** got 21 non-merge commits (mostly release work, docs updates, guard cleanups) - **dev** got extensive squad-sdk planning work (session logs, decisions, orchestration logs) -The branches effectively represent two different workstreams that happened in parallel: +The branches effectively represent two different SubSquads that happened in parallel: - **main**: Release management (v0.5.x), guard cleanup, archive notices - **dev**: Squad-SDK planning, CLI migration logs, PRD work diff --git a/.gitignore b/.gitignore index 9db759386..58aae88c2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ coverage/ .test-cli-* # Docs site generated files docs/dist/ -# Squad: workstream activation file (local to this machine) +# Squad: SubSquad activation file (local to this machine) .squad-workstream .squad/.first-run diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6770a2986..887446c0e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -101,7 +101,7 @@ These community members shaped Squad through issues, discussions, and feedback. | [@tomasherceg](https://github.com/tomasherceg) | #184 (Multi-PR commit isolation), #237 (CLI wiring bug) — worktree improvements and bug reports | | [@csharpfritz](https://github.com/csharpfritz) | #205 (Per-member model configuration) — model selection feature (shipped!) | | [@johnwc](https://github.com/johnwc) | #176 (Different repo support) — multi-repo workflows | -| [@tamirdresher](https://github.com/tamirdresher) | #200 (Squad Workstreams PRD), #237 (CLI wiring bug) — horizontal scaling concept and bug reports | +| [@tamirdresher](https://github.com/tamirdresher) | #200 (Squad SubSquads PRD), #237 (CLI wiring bug) — horizontal scaling concept and bug reports | | [@marchermans](https://github.com/marchermans) | #247 (Installation failure) — install bug report | | [@dkirby-ms](https://github.com/dkirby-ms) | #239 (Terminal flickering bug) — UX bug report | | [@EirikHaughom](https://github.com/EirikHaughom) | #223 (Model & reasoning configuration) — model config improvements | diff --git a/docs/blog/023-workstreams-horizontal-scaling.md b/docs/blog/023-subsquads-horizontal-scaling.md similarity index 61% rename from docs/blog/023-workstreams-horizontal-scaling.md rename to docs/blog/023-subsquads-horizontal-scaling.md index c15a9025a..6eb0dbed8 100644 --- a/docs/blog/023-workstreams-horizontal-scaling.md +++ b/docs/blog/023-subsquads-horizontal-scaling.md @@ -1,14 +1,14 @@ --- -title: "Workstreams — Scaling Squad Across Multiple Codespaces" +title: "SubSquads — Scaling Squad Across Multiple Codespaces" date: 2026-03-05 author: "Tamir Dresher (Community Contributor)" wave: null -tags: [squad, workstreams, scaling, codespaces, horizontal-scaling, multi-instance, community] +tags: [squad, subsquads, scaling, codespaces, horizontal-scaling, multi-instance, community] status: draft -hero: "Squad Workstreams lets you partition a repo's work across multiple Codespaces — each running its own scoped Squad instance. One repo, multiple AI teams, zero conflicts." +hero: "Squad SubSquads lets you partition a repo's work across multiple Codespaces — each running its own scoped Squad instance. One repo, multiple AI teams, zero conflicts." --- -# Workstreams — Scaling Squad Across Multiple Codespaces +# SubSquads — Scaling Squad Across Multiple Codespaces > Blog post #23 — A community contribution: horizontal scaling for Squad. @@ -16,11 +16,11 @@ hero: "Squad Workstreams lets you partition a repo's work across multiple Codesp We were building a multiplayer Tetris game with Squad. One team, 30 issues — UI, backend, cloud infra. Squad handled it fine at first, but as the issue count grew, a single Squad instance became a bottleneck. Agents stepped on each other in shared packages, there was no workflow enforcement, and we had no way to scope each Codespace to its slice of work. -So we built Workstreams. +So we built SubSquads. -## What Are Workstreams? +## What Are SubSquads? -Workstreams partition your repo's issues into labeled subsets. Each Codespace (or machine) runs one workstream, scoped to matching issues only. +SubSquads partition your repo's issues into labeled subsets. Each Codespace (or machine) runs one SubSquad, scoped to matching issues only. ``` ┌─────────────────────────────────────────────────┐ @@ -33,13 +33,13 @@ Workstreams partition your repo's issues into labeled subsets. Each Codespace (o │ └─────────────┘ └─────────────┘ └───────────┘ │ │ │ │ Ralph only picks up issues matching │ -│ the active workstream's label. │ +│ the active SubSquad's label. │ └─────────────────────────────────────────────────┘ ``` ## How It Works -**1. Define workstreams** in `.squad/workstreams.json`: +**1. Define SubSquads** in `.squad/streams.json`: ```json { @@ -61,14 +61,14 @@ Workstreams partition your repo's issues into labeled subsets. Each Codespace (o } ``` -**2. Activate a workstream:** +**2. Activate a SubSquad:** ```bash # Via environment variable (ideal for Codespaces) export SQUAD_TEAM=bridge # Or via CLI (local machines) -squad workstreams activate bridge +squad subsquads activate bridge ``` **3. Run Squad normally.** Ralph will only pick up issues labeled `team:bridge`. Agents enforce branch+PR workflow. `folderScope` guides where agents focus (advisory, not enforced — shared code is still accessible). @@ -77,7 +77,7 @@ squad workstreams activate bridge We validated this with [tamirdresher/squad-tetris](https://github.com/tamirdresher/squad-tetris) — 3 Codespaces, 30 issues, Star Trek TNG crew names: -| Codespace | Workstream | Squad Members | Focus | +| Codespace | SubSquad | Squad Members | Focus | |-----------|-----------|---------------|-------| | CS-1 | `ui` | Riker, Troi | React game board, animations | | CS-2 | `backend` | Geordi, Worf | WebSocket server, game state | @@ -88,32 +88,32 @@ We validated this with [tamirdresher/squad-tetris](https://github.com/tamirdresh ## CLI Commands ```bash -squad workstreams list # Show all configured workstreams -squad workstreams status # Activity per workstream (branches, PRs) -squad workstreams activate X # Activate a workstream for this machine +squad subsquads list # Show all configured SubSquads +squad subsquads status # Activity per SubSquad (branches, PRs) +squad subsquads activate X # Activate a SubSquad for this machine ``` ## Resolution Chain -Squad resolves the active workstream in priority order: +Squad resolves the active SubSquad in priority order: 1. `SQUAD_TEAM` environment variable 2. `.squad-workstream` file (written by `activate`, gitignored) -3. Auto-select if exactly one workstream is defined -4. No workstream → single-squad mode (backward compatible) +3. Auto-select if exactly one SubSquad is defined +4. No SubSquad → single-squad mode (backward compatible) ## Key Design Decisions - **folderScope is advisory** — agents prefer these directories but can modify shared code when needed - **Workflow enforcement** — `branch-per-issue` means every issue gets a branch and PR, never direct commits to main -- **Backward compatible** — repos without `workstreams.json` work exactly as before -- **Single-machine testing** — use `squad workstreams activate` to switch workstreams sequentially without needing multiple Codespaces +- **Backward compatible** — repos without `streams.json` work exactly as before +- **Single-machine testing** — use `squad subsquads activate` to switch SubSquads sequentially without needing multiple Codespaces ## What's Next -We're looking at cross-workstream coordination — a central dashboard showing all workstreams' activity, conflict detection for shared files, and auto-merge coordination. See the [PRD](https://github.com/bradygaster/squad/issues/200) for the full roadmap. +We're looking at cross-SubSquad coordination — a central dashboard showing all SubSquads' activity, conflict detection for shared files, and auto-merge coordination. See the [PRD](https://github.com/bradygaster/squad/issues/200) for the full roadmap. -Also: we haven't settled on the name yet! "Workstreams" is the working title, but we're considering alternatives like "Lanes", "Teams", or "Streams". If you have an opinion, let us know in the [discussion](https://github.com/bradygaster/squad/issues/200). +The community decided on the name "SubSquads" — each partition is a SubSquad of the main Squad. ## Try It @@ -124,10 +124,10 @@ npm install -g @bradygaster/squad-cli # Init in your repo squad init -# Create workstreams.json and label your issues +# Create streams.json and label your issues # Then activate and go -squad workstreams activate frontend +squad subsquads activate frontend squad start ``` -Full docs: [Scaling with Workstreams](../scenarios/scaling-workstreams.md) | [Multi-Codespace Setup](../scenarios/multi-codespace.md) | [Workstreams PRD](../specs/streams-prd.md) +Full docs: [Scaling with SubSquads](../scenarios/scaling-workstreams.md) | [Multi-Codespace Setup](../scenarios/multi-codespace.md) | [SubSquads PRD](../specs/streams-prd.md) diff --git a/docs/features/streams.md b/docs/features/streams.md index 1b7185199..e02fa803b 100644 --- a/docs/features/streams.md +++ b/docs/features/streams.md @@ -1,12 +1,12 @@ -# Squad Workstreams +# Squad SubSquads -> Scale Squad across multiple Codespaces by partitioning work into labeled workstreams. +> Scale Squad across multiple Codespaces by partitioning work into labeled SubSquads. -## What Are Workstreams? +## What Are SubSquads? -A **workstream** is a named partition of work within a Squad project. Each workstream targets a specific GitHub label (e.g., `team:ui`, `team:backend`) and optionally restricts agents to certain directories. Multiple Squad instances — each running in its own Codespace — can each activate a different workstream, enabling parallel work across teams. +A **SubSquad** is a named partition of work within a Squad project. Each SubSquad targets a specific GitHub label (e.g., `team:ui`, `team:backend`) and optionally restricts agents to certain directories. Multiple Squad instances — each running in its own Codespace — can each activate a different SubSquad, enabling parallel work across teams. -## Why Workstreams? +## Why SubSquads? Squad was originally designed for a single team per repository. As projects grow, a single Codespace becomes a bottleneck: @@ -14,11 +14,11 @@ Squad was originally designed for a single team per repository. As projects grow - **Context overload** — Ralph picks up all issues, not just the relevant ones - **Folder conflicts** — Multiple agents editing the same files causes merge pain -Workstreams solve this by giving each Codespace a scoped view of the project. +SubSquads solve this by giving each Codespace a scoped view of the project. ## Configuration -### 1. Create `.squad/workstreams.json` +### 1. Create `.squad/streams.json` ```json { @@ -49,9 +49,9 @@ Workstreams solve this by giving each Codespace a scoped view of the project. } ``` -### 2. Activate a Workstream +### 2. Activate a SubSquad -There are three ways to tell Squad which workstream to use: +There are three ways to tell Squad which SubSquad to use: #### Environment Variable (recommended for Codespaces) @@ -72,71 +72,73 @@ Set this in your Codespace's environment or devcontainer.json: #### .squad-workstream File (local activation) ```bash -squad workstreams activate ui-team +squad subsquads activate ui-team ``` This writes a `.squad-workstream` file (gitignored) so the setting is local to your machine. -#### Auto-select (single workstream) +#### Auto-select (single SubSquad) -If `workstreams.json` contains only one workstream, it's automatically selected. +If `streams.json` contains only one SubSquad, it's automatically selected. ### 3. Resolution Priority 1. `SQUAD_TEAM` env var (highest) 2. `.squad-workstream` file -3. Single-workstream auto-select -4. No workstream (classic single-squad mode) +3. Single-SubSquad auto-select +4. No SubSquad (classic single-squad mode) -## Workstream Definition Fields +## SubSquad Definition Fields | Field | Required | Description | |-------|----------|-------------| -| `name` | Yes | Unique workstream identifier (kebab-case) | +| `name` | Yes | Unique SubSquad identifier (kebab-case) | | `labelFilter` | Yes | GitHub label to filter issues | -| `folderScope` | No | Directories this workstream may modify | +| `folderScope` | No | Directories this SubSquad may modify | | `workflow` | No | `branch-per-issue` (default) or `direct` | | `description` | No | Human-readable purpose | ## CLI Reference ```bash -# List configured workstreams -squad workstreams list +# List configured SubSquads +squad subsquads list -# Show workstream activity (branches, PRs) -squad workstreams status +# Show SubSquad activity (branches, PRs) +squad subsquads status -# Activate a workstream locally -squad workstreams activate +# Activate a SubSquad locally +squad subsquads activate ``` +> **Note:** `squad workstreams` and `squad streams` are deprecated aliases for `squad subsquads`. + ## How It Works ### Triage (Ralph) -When a workstream is active, Ralph's triage only picks up issues labeled with the workstream's `labelFilter`. Unmatched issues are left for other workstreams or the main squad. +When a SubSquad is active, Ralph's triage only picks up issues labeled with the SubSquad's `labelFilter`. Unmatched issues are left for other SubSquads or the main squad. ### Workflow Enforcement - **branch-per-issue** (default): Every issue gets its own branch and PR. Agents never commit directly to main. -- **direct**: Agents may commit directly (useful for infra/ops workstreams). +- **direct**: Agents may commit directly (useful for infra/ops SubSquads). ### Folder Scope -When `folderScope` is set, agents should primarily modify files within those directories. However, `folderScope` is **advisory, not a hard lock** — agents may still touch shared files (types, configs, package exports) when their issue requires it. The real protection comes from `branch-per-issue` workflow: each issue gets its own branch, so two workstreams editing the same file won't conflict until merge time. +When `folderScope` is set, agents should primarily modify files within those directories. However, `folderScope` is **advisory, not a hard lock** — agents may still touch shared files (types, configs, package exports) when their issue requires it. The real protection comes from `branch-per-issue` workflow: each issue gets its own branch, so two SubSquads editing the same file won't conflict until merge time. -> **Tip:** If two workstreams' PRs touch the same file, Git resolves non-overlapping changes automatically. For semantic conflicts (incompatible API changes), use PR review to catch them. +> **Tip:** If two SubSquads' PRs touch the same file, Git resolves non-overlapping changes automatically. For semantic conflicts (incompatible API changes), use PR review to catch them. -### Cost Optimization: Single-Machine Multi-Workstream +### Cost Optimization: Single-Machine Multi-SubSquad -You don't need a separate Codespace per workstream. One machine can serve multiple workstreams: +You don't need a separate Codespace per SubSquad. One machine can serve multiple SubSquads: ```bash -# Switch between workstreams manually -squad workstreams activate ui-team # Ralph works team:ui issues +# Switch between SubSquads manually +squad subsquads activate ui-team # Ralph works team:ui issues # ... later ... -squad workstreams activate backend-team # now works team:backend issues +squad subsquads activate backend-team # now works team:backend issues ``` This gives you 1× Codespace cost instead of N×, at the expense of serial (not parallel) execution. Each issue still gets its own branch — no conflicts. diff --git a/docs/scenarios/multi-codespace.md b/docs/scenarios/multi-codespace.md index 873e6390f..93315c3ac 100644 --- a/docs/scenarios/multi-codespace.md +++ b/docs/scenarios/multi-codespace.md @@ -1,24 +1,24 @@ -# Multi-Codespace Setup with Squad Workstreams +# Multi-Codespace Setup with Squad SubSquads > End-to-end walkthrough of running multiple Squad instances across Codespaces. ## Background: The Tetris Experiment -We validated Squad Workstreams by building a multiplayer Tetris game using 3 Codespaces, each running a separate workstream: +We validated Squad SubSquads by building a multiplayer Tetris game using 3 Codespaces, each running a separate SubSquad: -| Codespace | Workstream | Label | Focus | +| Codespace | SubSquad | Label | Focus | |-----------|--------|-------|-------| | CS-1 | `ui-team` | `team:ui` | React game board, piece rendering, animations | | CS-2 | `backend-team` | `team:backend` | WebSocket server, game state, matchmaking | | CS-3 | `infra-team` | `team:infra` | CI/CD, Docker, deployment | -All three Codespaces shared the same repository. Each Squad instance only picked up issues matching its workstream's label. +All three Codespaces shared the same repository. Each Squad instance only picked up issues matching its SubSquad's label. ## Setup Steps -### 1. Create the workstreams config +### 1. Create the SubSquads config -In your repository, create `.squad/workstreams.json`: +In your repository, create `.squad/streams.json`: ```json { @@ -67,7 +67,7 @@ In `.devcontainer/devcontainer.json`, set the `SQUAD_TEAM` env var. For multiple ```bash export SQUAD_TEAM=ui-team -squad # launches with workstream context +squad # launches with SubSquad context ``` ### 3. Label your issues @@ -82,7 +82,7 @@ gh issue create --title "Add Docker compose for dev" --label "team:infra" ### 4. Launch Squad in each Codespace -Each Codespace runs `squad` normally. The workstream context is detected automatically: +Each Codespace runs `squad` normally. The SubSquad context is detected automatically: ```bash # In Codespace 1 (SQUAD_TEAM=ui-team) @@ -96,32 +96,32 @@ squad # → Agents only modify files in src/server, src/shared ``` -### 5. Monitor across workstreams +### 5. Monitor across SubSquads -Use the CLI from any Codespace to see all workstreams: +Use the CLI from any Codespace to see all SubSquads: ```bash -squad workstreams status +squad subsquads status ``` - + ## What Worked -- **Clear separation**: Each workstream had well-defined boundaries, minimizing merge conflicts +- **Clear separation**: Each SubSquad had well-defined boundaries, minimizing merge conflicts - **Parallel velocity**: 3x throughput vs. single-squad mode for independent work - **Label-based routing**: Simple, uses existing GitHub infrastructure ## What Didn't Work (Yet) -- **Cross-workstream dependencies**: When the UI team needed a backend API change, manual coordination was required +- **Cross-SubSquad dependencies**: When the UI team needed a backend API change, manual coordination was required - **Shared files**: `package.json`, `tsconfig.json`, and other root files caused occasional conflicts -- **No meta-coordinator**: No automated way to coordinate across workstreams (future work) +- **No meta-coordinator**: No automated way to coordinate across SubSquads (future work) ## Lessons Learned -1. **Keep workstreams independent** — design folder boundaries to minimize shared files -2. **Use branch-per-issue** — direct commits across workstreams cause merge hell -3. **Label everything** — unlabeled issues get lost between workstreams -4. **Start with 2 workstreams** — add more once the team finds its rhythm +1. **Keep SubSquads independent** — design folder boundaries to minimize shared files +2. **Use branch-per-issue** — direct commits across SubSquads cause merge hell +3. **Label everything** — unlabeled issues get lost between SubSquads +4. **Start with 2 SubSquads** — add more once the team finds its rhythm diff --git a/docs/scenarios/scaling-workstreams.md b/docs/scenarios/scaling-workstreams.md index 79ab13270..ef2e27a22 100644 --- a/docs/scenarios/scaling-workstreams.md +++ b/docs/scenarios/scaling-workstreams.md @@ -1,4 +1,4 @@ -# Scaling with Workstreams +# Scaling with SubSquads > Partition your repo's work across multiple Squad instances for horizontal scaling. @@ -10,9 +10,9 @@ A single Squad instance handles all issues in a repo. For large projects, this c - No workflow enforcement (agents commit directly to main) - No way to monitor multiple teams centrally -## The Solution: Workstreams +## The Solution: SubSquads -Workstreams partition a repo's issues into labeled subsets. Each Codespace (or machine) runs one workstream, scoped to its slice of work. +SubSquads partition a repo's issues into labeled subsets. Each Codespace (or machine) runs one SubSquad, scoped to its slice of work. ``` ┌─────────────────────────────────────────────────┐ @@ -26,15 +26,15 @@ Workstreams partition a repo's issues into labeled subsets. Each Codespace (or m │ └─────────────┘ └─────────────┘ └───────────┘ │ │ │ │ Each Squad instance only picks up issues │ -│ matching its workstream label. │ +│ matching its SubSquad label. │ └─────────────────────────────────────────────────┘ ``` ## Quick Start -### 1. Define workstreams +### 1. Define SubSquads -Create `.squad/workstreams.json`: +Create `.squad/streams.json`: ```json { @@ -64,21 +64,21 @@ Create `.squad/workstreams.json`: ### 2. Label your issues -Each issue gets a `team:*` label matching a workstream. Ralph will only pick up issues matching the active workstream's label. +Each issue gets a `team:*` label matching a SubSquad. Ralph will only pick up issues matching the active SubSquad's label. -### 3. Activate a workstream +### 3. Activate a SubSquad **Option A — Environment variable (Codespaces):** Set `SQUAD_TEAM=bridge` in the Codespace's environment. Squad auto-detects it on session start. **Option B — CLI activation (local):** ```bash -squad workstreams activate bridge +squad subsquads activate bridge ``` This writes a `.squad-workstream` file (gitignored — local to your machine). -**Option C — Single workstream auto-select:** -If `workstreams.json` defines only one workstream, it's auto-selected. +**Option C — Single SubSquad auto-select:** +If `streams.json` defines only one SubSquad, it's auto-selected. ### 4. Run Squad normally @@ -92,19 +92,22 @@ Ralph will only scan for issues with the `team:bridge` label. Agents will only p ## CLI Commands ```bash -# List configured workstreams -squad workstreams list +# List configured SubSquads +squad subsquads list -# Show activity per workstream (branches, PRs) -squad workstreams status +# Show activity per SubSquad (branches, PRs) +squad subsquads status -# Activate a workstream for this machine -squad workstreams activate engine +# Activate a SubSquad for this machine +squad subsquads activate engine -# Backward compat alias +# Deprecated aliases (still work) +squad workstreams list squad streams list ``` +> **Note:** `squad workstreams` and `squad streams` are deprecated aliases for `squad subsquads`. + ## Key Design Decisions ### folderScope is Advisory @@ -113,30 +116,30 @@ squad streams list ### Workflow Enforcement -Each workstream specifies a `workflow` (default: `branch-per-issue`). When active, agents: +Each SubSquad specifies a `workflow` (default: `branch-per-issue`). When active, agents: - Create a branch for every issue (`squad/{issue-number}-{slug}`) - Open a PR when work is ready - Never commit directly to main -### Single-Machine Multi-Workstream +### Single-Machine Multi-SubSquad -You don't need multiple Codespaces to test. Use `squad workstreams activate` to switch between workstreams sequentially on a single machine. +You don't need multiple Codespaces to test. Use `squad subsquads activate` to switch between SubSquads sequentially on a single machine. ## Resolution Chain -Squad resolves the active workstream in this order: +Squad resolves the active SubSquad in this order: 1. `SQUAD_TEAM` environment variable -2. `.squad-workstream` file (written by `squad workstreams activate`) -3. Auto-select if exactly one workstream is defined -4. No workstream → single-squad mode (backward compatible) +2. `.squad-workstream` file (written by `squad subsquads activate`) +3. Auto-select if exactly one SubSquad is defined +4. No SubSquad → single-squad mode (backward compatible) ## Monitoring -Use `squad workstreams status` to see all workstreams' activity: +Use `squad subsquads status` to see all SubSquads' activity: ``` -Configured Workstreams +Configured SubSquads Default workflow: branch-per-issue @@ -155,11 +158,11 @@ Configured Workstreams Workflow: branch-per-issue Folders: infra/, scripts/, .github/ - Active workstream resolved via: env + Active SubSquad resolved via: env ``` ## See Also - [Multi-Codespace Setup](multi-codespace.md) — Walkthrough of the Tetris experiment -- [Workstreams PRD](../specs/streams-prd.md) — Full specification -- [Workstreams Feature Guide](../features/streams.md) — API reference +- [SubSquads PRD](../specs/streams-prd.md) — Full specification +- [SubSquads Feature Guide](../features/streams.md) — API reference diff --git a/docs/specs/streams-prd.md b/docs/specs/streams-prd.md index 30b017489..6a9dc9fba 100644 --- a/docs/specs/streams-prd.md +++ b/docs/specs/streams-prd.md @@ -1,6 +1,6 @@ -# Workstreams PRD — Product Requirements Document +# SubSquads PRD — Product Requirements Document -> Scaling Squad across multiple Codespaces via labeled workstreams. +> Scaling Squad across multiple Codespaces via labeled SubSquads. ## Problem Statement @@ -13,37 +13,37 @@ Squad was designed for a single agent team per repository. In practice, larger p ### Validated by Experiment -We tested this with a 3-Codespace setup building a multiplayer Tetris game. Each Codespace ran Squad independently, manually filtering by GitHub labels. The results showed 3x throughput for independent workstreams, validating the approach. However, the manual coordination was error-prone and needed to be automated. +We tested this with a 3-Codespace setup building a multiplayer Tetris game. Each Codespace ran Squad independently, manually filtering by GitHub labels. The results showed 3x throughput for independent SubSquads, validating the approach. However, the manual coordination was error-prone and needed to be automated. ## Requirements ### Must Have (P0) -- [ ] **Workstream Definition**: Define workstreams in `.squad/workstreams.json` with name, label filter, folder scope, and workflow -- [ ] **Workstream Resolution**: Automatically detect active workstream from env var, file, or config -- [ ] **Label Filtering**: Ralph only triages issues matching the workstream's label -- [ ] **Folder Scoping**: Agents restrict modifications to workstream's folder scope -- [ ] **CLI Management**: `squad workstreams list|status|activate` commands -- [ ] **Init Integration**: `squad init` optionally generates workstreams config -- [ ] **Agent Template**: squad.agent.md includes workstream awareness instructions +- [ ] **SubSquad Definition**: Define SubSquads in `.squad/streams.json` with name, label filter, folder scope, and workflow +- [ ] **SubSquad Resolution**: Automatically detect active SubSquad from env var, file, or config +- [ ] **Label Filtering**: Ralph only triages issues matching the SubSquad's label +- [ ] **Folder Scoping**: Agents restrict modifications to SubSquad's folder scope +- [ ] **CLI Management**: `squad subsquads list|status|activate` commands +- [ ] **Init Integration**: `squad init` optionally generates SubSquads config +- [ ] **Agent Template**: squad.agent.md includes SubSquad awareness instructions ### Should Have (P1) -- [ ] **Workstream Status Dashboard**: Show PR/branch activity per workstream -- [ ] **Conflict Detection**: Warn when workstreams overlap on file paths +- [ ] **SubSquad Status Dashboard**: Show PR/branch activity per SubSquad +- [ ] **Conflict Detection**: Warn when SubSquads overlap on file paths - [ ] **Auto-labeling**: Suggest labels for new issues based on file paths ### Could Have (P2) -- [ ] **Meta-coordinator**: A coordinator that orchestrates across workstreams -- [ ] **Cross-workstream dependencies**: Track and resolve inter-workstream blockers -- [ ] **Workstream metrics**: Throughput, cycle time, merge conflict rate per workstream +- [ ] **Meta-coordinator**: A coordinator that orchestrates across SubSquads +- [ ] **Cross-SubSquad dependencies**: Track and resolve inter-SubSquad blockers +- [ ] **SubSquad metrics**: Throughput, cycle time, merge conflict rate per SubSquad ## Design Decisions ### 1. GitHub Labels as the Partition Key -**Decision**: Use GitHub labels (e.g., `team:ui`) to partition work across workstreams. +**Decision**: Use GitHub labels (e.g., `team:ui`) to partition work across SubSquads. **Rationale**: Labels are a first-class GitHub concept. They're visible in the UI, queryable via API, and already used by Squad for agent assignment. No new infrastructure needed. @@ -52,7 +52,7 @@ We tested this with a 3-Codespace setup building a multiplayer Tetris game. Each - Separate repositories — too heavy, loses monorepo benefits - Branch-based partitioning — branches are for code, not work items -### 2. File-Based Workstream Activation +### 2. File-Based SubSquad Activation **Decision**: Use `.squad-workstream` file (gitignored) for local activation, `SQUAD_TEAM` env var for Codespaces. @@ -62,40 +62,40 @@ We tested this with a 3-Codespace setup building a multiplayer Tetris game. Each **Decision**: Env var overrides file, which overrides config auto-select. -**Rationale**: In Codespaces, the env var is the most reliable signal. On local machines, the file is convenient. Config auto-select handles the simple case of a single workstream. +**Rationale**: In Codespaces, the env var is the most reliable signal. On local machines, the file is convenient. Config auto-select handles the simple case of a single SubSquad. -### 4. Synthesized Definitions for Unknown Workstreams +### 4. Synthesized Definitions for Unknown SubSquads -**Decision**: When `SQUAD_TEAM` or `.squad-workstream` specifies a workstream name not in the config, synthesize a minimal definition with `labelFilter: "team:{name}"`. +**Decision**: When `SQUAD_TEAM` or `.squad-workstream` specifies a SubSquad name not in the config, synthesize a minimal definition with `labelFilter: "team:{name}"`. -**Rationale**: Fail-open is better than fail-closed. Users should be able to set `SQUAD_TEAM=my-team` without needing to update `workstreams.json` first. The convention of `team:{name}` is predictable and consistent. +**Rationale**: Fail-open is better than fail-closed. Users should be able to set `SQUAD_TEAM=my-team` without needing to update `streams.json` first. The convention of `team:{name}` is predictable and consistent. ## Design Clarifications ### Overlapping Folder Scope -**Multiple workstreams MAY work on the same folders.** `folderScope` is an advisory guideline, not a hard lock. Reasons: +**Multiple SubSquads MAY work on the same folders.** `folderScope` is an advisory guideline, not a hard lock. Reasons: -- **Shared packages**: In a monorepo, `packages/shared/` might be touched by all workstreams. Preventing access would break legitimate work. -- **Interface contracts**: Backend and Frontend workstreams both need to update shared type definitions. -- **Branch isolation handles it**: Since each issue gets its own branch (`squad/{issue}-{slug}`), two workstreams editing the same file won't conflict until merge time — and Git resolves non-overlapping changes automatically. +- **Shared packages**: In a monorepo, `packages/shared/` might be touched by all SubSquads. Preventing access would break legitimate work. +- **Interface contracts**: Backend and Frontend SubSquads both need to update shared type definitions. +- **Branch isolation handles it**: Since each issue gets its own branch (`squad/{issue}-{slug}`), two SubSquads editing the same file won't conflict until merge time — and Git resolves non-overlapping changes automatically. -**When it breaks**: Semantic conflicts — two workstreams make incompatible API changes to the same file. This happened in the Tetris experiment where Backend restructured `game-engine/index.ts` exports while UI added color constants to the same file. Git merged cleanly but the result needed manual reconciliation. +**When it breaks**: Semantic conflicts — two SubSquads make incompatible API changes to the same file. This happened in the Tetris experiment where Backend restructured `game-engine/index.ts` exports while UI added color constants to the same file. Git merged cleanly but the result needed manual reconciliation. -**Mitigation (v2)**: Conflict detection — warn when two workstreams have open PRs touching the same file. Surface this in `squad workstreams status`. +**Mitigation (v2)**: Conflict detection — warn when two SubSquads have open PRs touching the same file. Surface this in `squad subsquads status`. -### Single-Machine Multi-Workstream +### Single-Machine Multi-SubSquad -**One machine can serve multiple workstreams to save costs.** Instead of 1 Codespace per workstream: +**One machine can serve multiple SubSquads to save costs.** Instead of 1 Codespace per SubSquad: ```bash # Sequential switching -squad workstreams activate ui-team # Ralph works team:ui issues +squad subsquads activate ui-team # Ralph works team:ui issues # ... switch when done ... -squad workstreams activate backend-team # now works team:backend issues +squad subsquads activate backend-team # now works team:backend issues ``` -**v2: Round-robin mode** — Ralph cycles through workstreams automatically: +**v2: Round-robin mode** — Ralph cycles through SubSquads automatically: ```json { "workstreams": [...], @@ -106,33 +106,33 @@ squad workstreams activate backend-team # now works team:backend issues | Approach | Cost | Speed | Isolation | |----------|------|-------|-----------| -| 1 Codespace per workstream | N× | Fastest (true parallel) | Best | +| 1 Codespace per SubSquad | N× | Fastest (true parallel) | Best | | 1 machine, manual switch | 1× | Serial | Good | | 1 machine, round-robin | 1× | Interleaved | Okay — branches isolate, context switches add overhead | -Branch-per-issue ensures no file conflicts regardless of approach — the "workstream" only determines which issues Ralph picks up. +Branch-per-issue ensures no file conflicts regardless of approach — the "SubSquad" only determines which issues Ralph picks up. ## Future Work ### Meta-Coordinator A "coordinator of coordinators" that: -- Monitors all workstreams for cross-cutting concerns -- Detects when one workstream's work blocks another +- Monitors all SubSquads for cross-cutting concerns +- Detects when one SubSquad's work blocks another - Suggests label assignments for ambiguous issues - Produces a unified status dashboard -### Cross-Workstream Dependencies +### Cross-SubSquad Dependencies -Track when a workstream needs work from another workstream: +Track when a SubSquad needs work from another SubSquad: - Automated detection via import graphs -- Cross-workstream issue linking +- Cross-SubSquad issue linking - Priority escalation for blocking dependencies -### Workstream Templates +### SubSquad Templates -Pre-built workstream configurations for common architectures: -- **Frontend/Backend** — 2 workstreams for web apps -- **Monorepo** — 1 workstream per package -- **Microservices** — 1 workstream per service -- **Feature teams** — dynamic workstreams per feature area +Pre-built SubSquad configurations for common architectures: +- **Frontend/Backend** — 2 SubSquads for web apps +- **Monorepo** — 1 SubSquad per package +- **Microservices** — 1 SubSquad per service +- **Feature teams** — dynamic SubSquads per feature area diff --git a/package.json b/package.json index e852992f2..0d6da8ffb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad", - "version": "0.8.23", + "version": "0.8.23.4", "private": true, "description": "Squad — Programmable multi-agent runtime for GitHub Copilot, built on @github/copilot-sdk", "type": "module", diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index ba5d62d4e..1fd6a32ec 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-cli", - "version": "0.8.23", + "version": "0.8.23.4", "description": "Squad CLI — Command-line interface for the Squad multi-agent runtime", "type": "module", "bin": { diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index 85d06c7f8..07e74cff3 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -174,8 +174,9 @@ async function main(): Promise { console.log(` Flags: --status, --check`); console.log(` ${BOLD}extract${RESET} Extract learnings from consult mode session`); console.log(` Flags: --dry-run, --clean, --yes, --accept-risks`); - console.log(` ${BOLD}workstreams${RESET} Manage Squad Workstreams (multi-Codespace scaling)`); - console.log(` Usage: workstreams >`); + console.log(` ${BOLD}subsquads${RESET} Manage Squad SubSquads (multi-Codespace scaling)`); + console.log(` Usage: subsquads >`); + console.log(` Aliases: workstreams, streams (deprecated)`); console.log(` ${BOLD}link${RESET} Link project to a remote team root`); console.log(` Usage: link `); console.log(` ${BOLD}build${RESET} Compile squad.config.ts into .squad/ markdown`); @@ -397,9 +398,9 @@ async function main(): Promise { return; } - if (cmd === 'workstreams' || cmd === 'streams') { - const { runWorkstreams } = await import('./cli/commands/streams.js'); - await runWorkstreams(process.cwd(), args.slice(1)); + if (cmd === 'subsquads' || cmd === 'workstreams' || cmd === 'streams') { + const { runSubSquads } = await import('./cli/commands/streams.js'); + await runSubSquads(process.cwd(), args.slice(1)); return; } diff --git a/packages/squad-cli/src/cli/commands/streams.ts b/packages/squad-cli/src/cli/commands/streams.ts index 411695d0f..cc086b465 100644 --- a/packages/squad-cli/src/cli/commands/streams.ts +++ b/packages/squad-cli/src/cli/commands/streams.ts @@ -1,17 +1,17 @@ /** - * CLI command: squad workstreams + * CLI command: squad subsquads * * Subcommands: - * list — Show configured workstreams - * status — Show activity per workstream (branches, PRs) - * activate — Write .squad-workstream file to activate a workstream + * list — Show configured SubSquads + * status — Show activity per SubSquad (branches, PRs) + * activate — Write .squad-workstream file to activate a SubSquad */ import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; -import { loadWorkstreamsConfig, resolveWorkstream } from '@bradygaster/squad-sdk'; -import type { WorkstreamDefinition } from '@bradygaster/squad-sdk'; +import { loadSubSquadsConfig, resolveSubSquad } from '@bradygaster/squad-sdk'; +import type { SubSquadDefinition } from '@bradygaster/squad-sdk'; const BOLD = '\x1b[1m'; const RESET = '\x1b[0m'; @@ -21,95 +21,98 @@ const YELLOW = '\x1b[33m'; const RED = '\x1b[31m'; /** - * Entry point for `squad workstreams` subcommand. + * Entry point for `squad subsquads` subcommand. */ -export async function runWorkstreams(cwd: string, args: string[]): Promise { +export async function runSubSquads(cwd: string, args: string[]): Promise { const sub = args[0]; if (!sub || sub === 'list') { - return listWorkstreams(cwd); + return listSubSquads(cwd); } if (sub === 'status') { - return showWorkstreamStatus(cwd); + return showSubSquadStatus(cwd); } if (sub === 'activate') { const name = args[1]; if (!name) { - console.error(`${RED}✗${RESET} Usage: squad workstreams activate `); + console.error(`${RED}✗${RESET} Usage: squad subsquads activate `); process.exit(1); } - return activateWorkstream(cwd, name); + return activateSubSquad(cwd, name); } - console.error(`${RED}✗${RESET} Unknown workstreams subcommand: ${sub}`); - console.log(`\nUsage: squad workstreams >`); + console.error(`${RED}✗${RESET} Unknown subsquads subcommand: ${sub}`); + console.log(`\nUsage: squad subsquads >`); process.exit(1); } -/** @deprecated Use runWorkstreams instead */ -export const runStreams = runWorkstreams; +/** @deprecated Use runSubSquads instead */ +export const runWorkstreams = runSubSquads; + +/** @deprecated Use runSubSquads instead */ +export const runStreams = runSubSquads; /** - * List configured workstreams. + * List configured SubSquads. */ -function listWorkstreams(cwd: string): void { - const config = loadWorkstreamsConfig(cwd); - const active = resolveWorkstream(cwd); +function listSubSquads(cwd: string): void { + const config = loadSubSquadsConfig(cwd); + const active = resolveSubSquad(cwd); if (!config || config.workstreams.length === 0) { - console.log(`\n${DIM}No workstreams configured.${RESET}`); - console.log(`${DIM}Create .squad/workstreams.json to define workstreams.${RESET}\n`); + console.log(`\n${DIM}No SubSquads configured.${RESET}`); + console.log(`${DIM}Create .squad/streams.json to define SubSquads.${RESET}\n`); return; } - console.log(`\n${BOLD}Configured Workstreams${RESET}\n`); + console.log(`\n${BOLD}Configured SubSquads${RESET}\n`); console.log(` Default workflow: ${config.defaultWorkflow}\n`); - for (const workstream of config.workstreams) { - const isActive = active?.name === workstream.name; + for (const subsquad of config.workstreams) { + const isActive = active?.name === subsquad.name; const marker = isActive ? `${GREEN}● active${RESET}` : `${DIM}○${RESET}`; - const workflow = workstream.workflow ?? config.defaultWorkflow; - console.log(` ${marker} ${BOLD}${workstream.name}${RESET}`); - console.log(` Label: ${workstream.labelFilter}`); + const workflow = subsquad.workflow ?? config.defaultWorkflow; + console.log(` ${marker} ${BOLD}${subsquad.name}${RESET}`); + console.log(` Label: ${subsquad.labelFilter}`); console.log(` Workflow: ${workflow}`); - if (workstream.folderScope?.length) { - console.log(` Folders: ${workstream.folderScope.join(', ')}`); + if (subsquad.folderScope?.length) { + console.log(` Folders: ${subsquad.folderScope.join(', ')}`); } - if (workstream.description) { - console.log(` ${DIM}${workstream.description}${RESET}`); + if (subsquad.description) { + console.log(` ${DIM}${subsquad.description}${RESET}`); } console.log(); } if (active) { - console.log(` ${DIM}Active workstream resolved via: ${active.source}${RESET}\n`); + console.log(` ${DIM}Active SubSquad resolved via: ${active.source}${RESET}\n`); } } /** - * Show activity per workstream (branches, PRs via gh CLI). + * Show activity per SubSquad (branches, PRs via gh CLI). */ -function showWorkstreamStatus(cwd: string): void { - const config = loadWorkstreamsConfig(cwd); - const active = resolveWorkstream(cwd); +function showSubSquadStatus(cwd: string): void { + const config = loadSubSquadsConfig(cwd); + const active = resolveSubSquad(cwd); if (!config || config.workstreams.length === 0) { - console.log(`\n${DIM}No workstreams configured.${RESET}\n`); + console.log(`\n${DIM}No SubSquads configured.${RESET}\n`); return; } - console.log(`\n${BOLD}Workstream Status${RESET}\n`); + console.log(`\n${BOLD}SubSquad Status${RESET}\n`); - for (const workstream of config.workstreams) { - const isActive = active?.name === workstream.name; + for (const subsquad of config.workstreams) { + const isActive = active?.name === subsquad.name; const marker = isActive ? `${GREEN}●${RESET}` : `${DIM}○${RESET}`; - console.log(` ${marker} ${BOLD}${workstream.name}${RESET} (${workstream.labelFilter})`); + console.log(` ${marker} ${BOLD}${subsquad.name}${RESET} (${subsquad.labelFilter})`); // Try to get PR and branch info via gh CLI try { const result = spawnSync( 'gh', - ['pr', 'list', '--label', workstream.labelFilter, '--json', 'number,title,state', '--limit', '5'], + ['pr', 'list', '--label', subsquad.labelFilter, '--json', 'number,title,state', '--limit', '5'], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, ); const prOutput = result.stdout ?? ''; @@ -130,7 +133,7 @@ function showWorkstreamStatus(cwd: string): void { try { const result = spawnSync( 'git', - ['branch', '--list', `*${workstream.name}*`], + ['branch', '--list', `*${subsquad.name}*`], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, ); const branchOutput = result.stdout ?? ''; @@ -150,16 +153,16 @@ function showWorkstreamStatus(cwd: string): void { } /** - * Activate a workstream by writing .squad-workstream file. + * Activate a SubSquad by writing .squad-workstream file. */ -function activateWorkstream(cwd: string, name: string): void { - const config = loadWorkstreamsConfig(cwd); +function activateSubSquad(cwd: string, name: string): void { + const config = loadSubSquadsConfig(cwd); - // Validate the workstream exists in config (warn if not, but still allow) + // Validate the SubSquad exists in config (warn if not, but still allow) if (config) { const found = config.workstreams.find(s => s.name === name); if (!found) { - console.log(`${YELLOW}⚠${RESET} Workstream "${name}" not found in .squad/workstreams.json`); + console.log(`${YELLOW}⚠${RESET} SubSquad "${name}" not found in .squad/streams.json`); console.log(` Available: ${config.workstreams.map(s => s.name).join(', ')}`); console.log(` Writing .squad-workstream anyway...\n`); } @@ -167,7 +170,7 @@ function activateWorkstream(cwd: string, name: string): void { const workstreamFilePath = path.join(cwd, '.squad-workstream'); fs.writeFileSync(workstreamFilePath, name + '\n', 'utf-8'); - console.log(`${GREEN}✓${RESET} Activated workstream: ${BOLD}${name}${RESET}`); + console.log(`${GREEN}✓${RESET} Activated SubSquad: ${BOLD}${name}${RESET}`); console.log(` Written to: ${workstreamFilePath}`); console.log(`${DIM} (This file is gitignored — it's local to your machine/Codespace)${RESET}\n`); } diff --git a/packages/squad-sdk/package.json b/packages/squad-sdk/package.json index 1c2456015..b847a4c85 100644 --- a/packages/squad-sdk/package.json +++ b/packages/squad-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-sdk", - "version": "0.8.23", + "version": "0.8.23.4", "description": "Squad SDK — Programmable multi-agent runtime for GitHub Copilot", "type": "module", "main": "./dist/index.js", diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 80f51c09a..2b068ef60 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -15,7 +15,7 @@ import { existsSync, cpSync, statSync, mkdirSync, writeFileSync, readFileSync, r import { execFileSync } from 'node:child_process'; import { MODELS } from '../runtime/constants.js'; import type { SquadConfig, ModelSelectionConfig, RoutingConfig } from '../runtime/config.js'; -import type { WorkstreamDefinition } from '../streams/types.js'; +import type { SubSquadDefinition } from '../streams/types.js'; // ============================================================================ // Template Resolution @@ -111,8 +111,8 @@ export interface InitOptions { prompt?: string; /** If true, disable extraction from consult sessions (read-only consultations) */ extractionDisabled?: boolean; - /** Optional workstream definitions — generates .squad/workstreams.json when provided */ - streams?: WorkstreamDefinition[]; + /** Optional SubSquad definitions — generates .squad/workstreams.json when provided */ + streams?: SubSquadDefinition[]; } /** @@ -971,20 +971,20 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre } // ------------------------------------------------------------------------- - // Generate .squad/workstreams.json (when streams provided) + // Generate .squad/workstreams.json (when SubSquads provided) // ------------------------------------------------------------------------- if (options.streams && options.streams.length > 0) { - const workstreamsConfig = { + const subsquadsConfig = { workstreams: options.streams, defaultWorkflow: 'branch-per-issue', }; const workstreamsPath = join(squadDir, 'workstreams.json'); - await writeIfNotExists(workstreamsPath, JSON.stringify(workstreamsConfig, null, 2) + '\n'); + await writeIfNotExists(workstreamsPath, JSON.stringify(subsquadsConfig, null, 2) + '\n'); } // ------------------------------------------------------------------------- - // Add .squad-workstream to .gitignore + // Add .squad-workstream to .gitignore (SubSquad activation file) // ------------------------------------------------------------------------- { @@ -995,7 +995,7 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre } if (!currentIgnore.includes(workstreamIgnoreEntry)) { const block = (currentIgnore && !currentIgnore.endsWith('\n') ? '\n' : '') - + '# Squad: workstream activation file (local to this machine)\n' + + '# Squad: SubSquad activation file (local to this machine)\n' + workstreamIgnoreEntry + '\n'; await appendFile(gitignorePath, block); createdFiles.push(toRelativePath(gitignorePath)); diff --git a/packages/squad-sdk/src/streams/filter.ts b/packages/squad-sdk/src/streams/filter.ts index 44b913e0e..948467cc3 100644 --- a/packages/squad-sdk/src/streams/filter.ts +++ b/packages/squad-sdk/src/streams/filter.ts @@ -1,39 +1,42 @@ /** - * Workstream-Aware Issue Filtering + * SubSquad-Aware Issue Filtering * - * Filters GitHub issues to only those matching a workstream's labelFilter. - * Intended to scope work to the active workstream during triage. + * Filters GitHub issues to only those matching a SubSquad's labelFilter. + * Intended to scope work to the active SubSquad during triage. * * @module streams/filter */ -import type { ResolvedWorkstream } from './types.js'; +import type { ResolvedSubSquad } from './types.js'; /** Minimal issue shape for filtering. */ -export interface WorkstreamIssue { +export interface SubSquadIssue { number: number; title: string; labels: Array<{ name: string }>; } -/** @deprecated Use WorkstreamIssue instead */ -export type StreamIssue = WorkstreamIssue; +/** @deprecated Use SubSquadIssue instead */ +export type WorkstreamIssue = SubSquadIssue; + +/** @deprecated Use SubSquadIssue instead */ +export type StreamIssue = SubSquadIssue; /** - * Filter issues to only those matching the workstream's label filter. + * Filter issues to only those matching the SubSquad's label filter. * - * Matching is case-insensitive. If the workstream has no labelFilter, + * Matching is case-insensitive. If the SubSquad has no labelFilter, * all issues are returned (passthrough). * * @param issues - Array of issues to filter - * @param workstream - The resolved workstream to filter by - * @returns Filtered array of issues matching the workstream's label + * @param subsquad - The resolved SubSquad to filter by + * @returns Filtered array of issues matching the SubSquad's label */ -export function filterIssuesByWorkstream( - issues: WorkstreamIssue[], - workstream: ResolvedWorkstream, -): WorkstreamIssue[] { - const filter = workstream.definition.labelFilter; +export function filterIssuesBySubSquad( + issues: SubSquadIssue[], + subsquad: ResolvedSubSquad, +): SubSquadIssue[] { + const filter = subsquad.definition.labelFilter; if (!filter) { return issues; } @@ -44,5 +47,8 @@ export function filterIssuesByWorkstream( ); } -/** @deprecated Use filterIssuesByWorkstream instead */ -export const filterIssuesByStream = filterIssuesByWorkstream; +/** @deprecated Use filterIssuesBySubSquad instead */ +export const filterIssuesByWorkstream = filterIssuesBySubSquad; + +/** @deprecated Use filterIssuesBySubSquad instead */ +export const filterIssuesByStream = filterIssuesBySubSquad; diff --git a/packages/squad-sdk/src/streams/index.ts b/packages/squad-sdk/src/streams/index.ts index aafc995ec..f79dfea2a 100644 --- a/packages/squad-sdk/src/streams/index.ts +++ b/packages/squad-sdk/src/streams/index.ts @@ -1,5 +1,5 @@ /** - * Workstreams module — barrel exports. + * SubSquads module — barrel exports. * * @module streams */ diff --git a/packages/squad-sdk/src/streams/resolver.ts b/packages/squad-sdk/src/streams/resolver.ts index 0a1d17303..09059f227 100644 --- a/packages/squad-sdk/src/streams/resolver.ts +++ b/packages/squad-sdk/src/streams/resolver.ts @@ -1,26 +1,26 @@ /** - * Workstream Resolver — Resolves which workstream is active. + * SubSquad Resolver — Resolves which SubSquad is active. * * Resolution order: - * 1. SQUAD_TEAM env var → look up in workstreams config - * 2. .squad-workstream file (gitignored) → contains workstream name - * 3. If exactly one workstream is defined in the config, auto-select that workstream - * 4. null (no active workstream — single-squad mode / no workstreams) + * 1. SQUAD_TEAM env var → look up in SubSquads config + * 2. .squad-workstream file (gitignored) → contains SubSquad name + * 3. If exactly one SubSquad is defined in the config, auto-select that SubSquad + * 4. null (no active SubSquad — single-squad mode / no SubSquads) * * @module streams/resolver */ import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; -import type { WorkstreamConfig, WorkstreamDefinition, ResolvedWorkstream } from './types.js'; +import type { SubSquadConfig, SubSquadDefinition, ResolvedSubSquad } from './types.js'; /** - * Load workstreams configuration from .squad/workstreams.json. + * Load SubSquads configuration from .squad/workstreams.json. * * @param squadRoot - Root directory of the project (where .squad/ lives) - * @returns Parsed WorkstreamConfig or null if not found / invalid + * @returns Parsed SubSquadConfig or null if not found / invalid */ -export function loadWorkstreamsConfig(squadRoot: string): WorkstreamConfig | null { +export function loadSubSquadsConfig(squadRoot: string): SubSquadConfig | null { const configPath = join(squadRoot, '.squad', 'workstreams.json'); if (!existsSync(configPath)) { return null; @@ -52,7 +52,7 @@ export function loadWorkstreamsConfig(squadRoot: string): WorkstreamConfig | nul return null; } - const workstreams: WorkstreamDefinition[] = workstreamsRaw + const workstreams: SubSquadDefinition[] = workstreamsRaw .filter(entry => entry && typeof entry === 'object') .map(entry => { const e = entry as { @@ -86,9 +86,9 @@ export function loadWorkstreamsConfig(squadRoot: string): WorkstreamConfig | nul normalized.description = e.description; } - return normalized as unknown as WorkstreamDefinition; + return normalized as unknown as SubSquadDefinition; }) - .filter((s): s is WorkstreamDefinition => s !== null); + .filter((s): s is SubSquadDefinition => s !== null); if (workstreams.length === 0) { return null; @@ -100,35 +100,38 @@ export function loadWorkstreamsConfig(squadRoot: string): WorkstreamConfig | nul } } -/** @deprecated Use loadWorkstreamsConfig instead */ -export const loadStreamsConfig = loadWorkstreamsConfig; +/** @deprecated Use loadSubSquadsConfig instead */ +export const loadWorkstreamsConfig = loadSubSquadsConfig; + +/** @deprecated Use loadSubSquadsConfig instead */ +export const loadStreamsConfig = loadSubSquadsConfig; /** - * Find a workstream definition by name in a config. + * Find a SubSquad definition by name in a config. */ -function findWorkstream(config: WorkstreamConfig, name: string): WorkstreamDefinition | undefined { +function findSubSquad(config: SubSquadConfig, name: string): SubSquadDefinition | undefined { return config.workstreams.find(s => s.name === name); } /** - * Resolve which workstream is active for the current environment. + * Resolve which SubSquad is active for the current environment. * * @param squadRoot - Root directory of the project - * @returns ResolvedWorkstream or null if no workstream is active + * @returns ResolvedSubSquad or null if no SubSquad is active */ -export function resolveWorkstream(squadRoot: string): ResolvedWorkstream | null { - const config = loadWorkstreamsConfig(squadRoot); +export function resolveSubSquad(squadRoot: string): ResolvedSubSquad | null { + const config = loadSubSquadsConfig(squadRoot); // 1. SQUAD_TEAM env var const envTeam = process.env.SQUAD_TEAM; if (envTeam) { if (config) { - const def = findWorkstream(config, envTeam); + const def = findSubSquad(config, envTeam); if (def) { return { name: envTeam, definition: def, source: 'env' }; } } - // Env var set but no matching workstream config — synthesize a minimal definition + // Env var set but no matching SubSquad config — synthesize a minimal definition return { name: envTeam, definition: { @@ -143,20 +146,20 @@ export function resolveWorkstream(squadRoot: string): ResolvedWorkstream | null const workstreamFilePath = join(squadRoot, '.squad-workstream'); if (existsSync(workstreamFilePath)) { try { - const workstreamName = readFileSync(workstreamFilePath, 'utf-8').trim(); - if (workstreamName) { + const subsquadName = readFileSync(workstreamFilePath, 'utf-8').trim(); + if (subsquadName) { if (config) { - const def = findWorkstream(config, workstreamName); + const def = findSubSquad(config, subsquadName); if (def) { - return { name: workstreamName, definition: def, source: 'file' }; + return { name: subsquadName, definition: def, source: 'file' }; } } // File exists but no config — synthesize return { - name: workstreamName, + name: subsquadName, definition: { - name: workstreamName, - labelFilter: `team:${workstreamName}`, + name: subsquadName, + labelFilter: `team:${subsquadName}`, }, source: 'file', }; @@ -166,28 +169,34 @@ export function resolveWorkstream(squadRoot: string): ResolvedWorkstream | null } } - // 3. If exactly one workstream is defined, auto-select it + // 3. If exactly one SubSquad is defined, auto-select it if (config && config.workstreams.length === 1) { const def = config.workstreams[0]!; return { name: def.name, definition: def, source: 'config' }; } - // 4. No workstream detected + // 4. No SubSquad detected return null; } -/** @deprecated Use resolveWorkstream instead */ -export const resolveStream = resolveWorkstream; +/** @deprecated Use resolveSubSquad instead */ +export const resolveWorkstream = resolveSubSquad; + +/** @deprecated Use resolveSubSquad instead */ +export const resolveStream = resolveSubSquad; /** - * Get the GitHub label filter string for a resolved workstream. + * Get the GitHub label filter string for a resolved SubSquad. * - * @param workstream - The resolved workstream + * @param subsquad - The resolved SubSquad * @returns Label filter string (e.g., "team:ui") */ -export function getWorkstreamLabelFilter(workstream: ResolvedWorkstream): string { - return workstream.definition.labelFilter; +export function getSubSquadLabelFilter(subsquad: ResolvedSubSquad): string { + return subsquad.definition.labelFilter; } -/** @deprecated Use getWorkstreamLabelFilter instead */ -export const getStreamLabelFilter = getWorkstreamLabelFilter; +/** @deprecated Use getSubSquadLabelFilter instead */ +export const getWorkstreamLabelFilter = getSubSquadLabelFilter; + +/** @deprecated Use getSubSquadLabelFilter instead */ +export const getStreamLabelFilter = getSubSquadLabelFilter; diff --git a/packages/squad-sdk/src/streams/types.ts b/packages/squad-sdk/src/streams/types.ts index dcd9ee4e0..e04559545 100644 --- a/packages/squad-sdk/src/streams/types.ts +++ b/packages/squad-sdk/src/streams/types.ts @@ -1,15 +1,15 @@ /** - * Workstream Types — Type definitions for Squad Workstreams. + * SubSquad Types — Type definitions for Squad SubSquads. * - * Workstreams enable horizontal scaling by allowing multiple Squad instances + * SubSquads enable horizontal scaling by allowing multiple Squad instances * (e.g., in different Codespaces) to each handle a scoped subset of work. * * @module streams/types */ -/** Definition of a single workstream (team partition). */ -export interface WorkstreamDefinition { - /** Workstream name, e.g., "ui-team", "backend-team" */ +/** Definition of a single SubSquad (team partition). */ +export interface SubSquadDefinition { + /** SubSquad name, e.g., "ui-team", "backend-team" */ name: string; /** GitHub label to filter issues by, e.g., "team:ui" */ labelFilter: string; @@ -17,33 +17,42 @@ export interface WorkstreamDefinition { folderScope?: string[]; /** Workflow mode. Default: branch-per-issue */ workflow?: 'branch-per-issue' | 'direct'; - /** Human-readable description of this workstream's purpose */ + /** Human-readable description of this SubSquad's purpose */ description?: string; } -/** @deprecated Use WorkstreamDefinition instead */ -export type StreamDefinition = WorkstreamDefinition; +/** @deprecated Use SubSquadDefinition instead */ +export type WorkstreamDefinition = SubSquadDefinition; -/** Top-level workstreams configuration (stored in .squad/workstreams.json). */ -export interface WorkstreamConfig { - /** All configured workstreams */ - workstreams: WorkstreamDefinition[]; - /** Default workflow for workstreams that don't specify one */ +/** @deprecated Use SubSquadDefinition instead */ +export type StreamDefinition = SubSquadDefinition; + +/** Top-level SubSquads configuration (stored in .squad/streams.json). */ +export interface SubSquadConfig { + /** All configured SubSquads */ + workstreams: SubSquadDefinition[]; + /** Default workflow for SubSquads that don't specify one */ defaultWorkflow: 'branch-per-issue' | 'direct'; } -/** @deprecated Use WorkstreamConfig instead */ -export type StreamConfig = WorkstreamConfig; +/** @deprecated Use SubSquadConfig instead */ +export type WorkstreamConfig = SubSquadConfig; + +/** @deprecated Use SubSquadConfig instead */ +export type StreamConfig = SubSquadConfig; -/** A resolved workstream with provenance information. */ -export interface ResolvedWorkstream { - /** Workstream name */ +/** A resolved SubSquad with provenance information. */ +export interface ResolvedSubSquad { + /** SubSquad name */ name: string; - /** Full workstream definition */ - definition: WorkstreamDefinition; - /** How this workstream was resolved */ + /** Full SubSquad definition */ + definition: SubSquadDefinition; + /** How this SubSquad was resolved */ source: 'env' | 'file' | 'config'; } -/** @deprecated Use ResolvedWorkstream instead */ -export type ResolvedStream = ResolvedWorkstream; +/** @deprecated Use ResolvedSubSquad instead */ +export type ResolvedWorkstream = ResolvedSubSquad; + +/** @deprecated Use ResolvedSubSquad instead */ +export type ResolvedStream = ResolvedSubSquad; diff --git a/packages/squad-sdk/src/types.ts b/packages/squad-sdk/src/types.ts index 53ca74880..2c5682656 100644 --- a/packages/squad-sdk/src/types.ts +++ b/packages/squad-sdk/src/types.ts @@ -58,9 +58,15 @@ export type { SquadEntry } from './multi-squad.js'; export type { MultiSquadConfig } from './multi-squad.js'; export type { SquadInfo } from './multi-squad.js'; -// --- Workstream types (streams/types.ts) --- +// --- SubSquad types (streams/types.ts) --- +export type { SubSquadDefinition } from './streams/types.js'; +export type { SubSquadConfig } from './streams/types.js'; +export type { ResolvedSubSquad } from './streams/types.js'; +/** @deprecated Use SubSquadDefinition */ export type { WorkstreamDefinition } from './streams/types.js'; +/** @deprecated Use SubSquadConfig */ export type { WorkstreamConfig } from './streams/types.js'; +/** @deprecated Use ResolvedSubSquad */ export type { ResolvedWorkstream } from './streams/types.js'; /** @deprecated aliases */ export type { StreamDefinition } from './streams/types.js'; diff --git a/templates/squad.agent.md b/templates/squad.agent.md index 65380b2eb..66027b498 100644 --- a/templates/squad.agent.md +++ b/templates/squad.agent.md @@ -111,18 +111,18 @@ When triggered: **Casting migration check:** If `.squad/team.md` exists but `.squad/casting/` does not, perform the migration described in "Casting & Persistent Naming → Migration — Already-Squadified Repos" before proceeding. -### Workstream Awareness - -On session start, resolve workstream context using the Workstream resolver: -1. Check for a `.squad-workstream` file in the repo root. If present, activate the referenced workstream. -2. If no `.squad-workstream` is present, read the `SQUAD_TEAM` env var (if set) and resolve the matching workstream from `.squad/workstreams.json`. -3. If there is exactly one workstream defined in `.squad/workstreams.json` and nothing else selects a workstream, auto-select it. -4. When a workstream is active: - - Apply the workstream's `labelFilter` — Ralph should normally only pick up issues matching this label unless the user explicitly directs otherwise. - - Apply the workstream's `workflow` — if `branch-per-issue`, enforce creating a branch and PR for every issue (never commit directly to main). - - Apply the workstream's `folderScope` as an advisory focus area: prefer modifying files in these directories, and call out when you intentionally work outside them (e.g., to update shared dependencies or cross-cutting code). - -If no workstream is resolved, operate in default single-squad mode. +### SubSquad Awareness + +On session start, resolve SubSquad context using the SubSquad resolver: +1. Check for a `.squad-workstream` file in the repo root. If present, activate the referenced SubSquad. +2. If no `.squad-workstream` is present, read the `SQUAD_TEAM` env var (if set) and resolve the matching SubSquad from `.squad/streams.json`. +3. If there is exactly one SubSquad defined in `.squad/streams.json` and nothing else selects a SubSquad, auto-select it. +4. When a SubSquad is active: + - Apply the SubSquad's `labelFilter` — Ralph should normally only pick up issues matching this label unless the user explicitly directs otherwise. + - Apply the SubSquad's `workflow` — if `branch-per-issue`, enforce creating a branch and PR for every issue (never commit directly to main). + - Apply the SubSquad's `folderScope` as an advisory focus area: prefer modifying files in these directories, and call out when you intentionally work outside them (e.g., to update shared dependencies or cross-cutting code). + +If no SubSquad is resolved, operate in default single-squad mode. ### Issue Awareness diff --git a/test/streams.test.ts b/test/streams.test.ts index fa93cf53d..a40c09916 100644 --- a/test/streams.test.ts +++ b/test/streams.test.ts @@ -1,14 +1,14 @@ /** - * Squad Workstreams — Comprehensive Tests + * Squad SubSquads — Comprehensive Tests * * Tests cover: - * - Workstream types (compile-time, verified via usage) - * - Workstream resolution (env var, file, config, fallback) + * - SubSquad types (compile-time, verified via usage) + * - SubSquad resolution (env var, file, config, fallback) * - Label-based filtering (match, no match, multiple labels, case insensitive) * - Config loading / validation * - CLI activate command (writes .squad-workstream) - * - Init with workstreams (generates workstreams.json) - * - Edge cases (empty workstreams, invalid JSON, missing env, passthrough) + * - Init with SubSquads (generates workstreams.json) + * - Edge cases (empty SubSquads, invalid JSON, missing env, passthrough) */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -17,6 +17,11 @@ import path from 'node:path'; import os from 'node:os'; import { + loadSubSquadsConfig, + resolveSubSquad, + getSubSquadLabelFilter, + filterIssuesBySubSquad, + // Verify backward-compat aliases still exist loadWorkstreamsConfig, resolveWorkstream, getWorkstreamLabelFilter, @@ -24,6 +29,11 @@ import { } from '../packages/squad-sdk/src/streams/index.js'; import type { + SubSquadDefinition, + SubSquadConfig, + ResolvedSubSquad, + SubSquadIssue, + // Verify deprecated type aliases still exist WorkstreamDefinition, WorkstreamConfig, ResolvedWorkstream, @@ -38,7 +48,7 @@ function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'squad-workstreams-test-')); } -function writeSquadWorkstreamsConfig(root: string, config: WorkstreamConfig): void { +function writeSquadWorkstreamsConfig(root: string, config: SubSquadConfig): void { const squadDir = path.join(root, '.squad'); fs.mkdirSync(squadDir, { recursive: true }); fs.writeFileSync(path.join(squadDir, 'workstreams.json'), JSON.stringify(config, null, 2), 'utf-8'); @@ -48,7 +58,7 @@ function writeSquadWorkstreamFile(root: string, name: string): void { fs.writeFileSync(path.join(root, '.squad-workstream'), name + '\n', 'utf-8'); } -const SAMPLE_CONFIG: WorkstreamConfig = { +const SAMPLE_CONFIG: SubSquadConfig = { workstreams: [ { name: 'ui-team', labelFilter: 'team:ui', folderScope: ['apps/web'], workflow: 'branch-per-issue', description: 'UI specialists' }, { name: 'backend-team', labelFilter: 'team:backend', folderScope: ['apps/api'], workflow: 'direct' }, @@ -57,7 +67,7 @@ const SAMPLE_CONFIG: WorkstreamConfig = { defaultWorkflow: 'branch-per-issue', }; -const SAMPLE_ISSUES: WorkstreamIssue[] = [ +const SAMPLE_ISSUES: SubSquadIssue[] = [ { number: 1, title: 'Fix button color', labels: [{ name: 'team:ui' }, { name: 'bug' }] }, { number: 2, title: 'Add REST endpoint', labels: [{ name: 'team:backend' }] }, { number: 3, title: 'Setup CI', labels: [{ name: 'team:infra' }] }, @@ -69,19 +79,19 @@ const SAMPLE_ISSUES: WorkstreamIssue[] = [ // loadStreamsConfig // ============================================================================ -describe('loadWorkstreamsConfig', () => { +describe('loadSubSquadsConfig', () => { let tmpDir: string; beforeEach(() => { tmpDir = makeTmpDir(); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('returns null when .squad/workstreams.json does not exist', () => { - expect(loadWorkstreamsConfig(tmpDir)).toBeNull(); + expect(loadSubSquadsConfig(tmpDir)).toBeNull(); }); - it('loads a valid workstreams config', () => { + it('loads a valid SubSquads config', () => { writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); - const result = loadWorkstreamsConfig(tmpDir); + const result = loadSubSquadsConfig(tmpDir); expect(result).not.toBeNull(); expect(result!.workstreams).toHaveLength(3); expect(result!.defaultWorkflow).toBe('branch-per-issue'); @@ -91,33 +101,33 @@ describe('loadWorkstreamsConfig', () => { const squadDir = path.join(tmpDir, '.squad'); fs.mkdirSync(squadDir, { recursive: true }); fs.writeFileSync(path.join(squadDir, 'workstreams.json'), '{invalid', 'utf-8'); - expect(loadWorkstreamsConfig(tmpDir)).toBeNull(); + expect(loadSubSquadsConfig(tmpDir)).toBeNull(); }); it('returns null when workstreams array is missing', () => { const squadDir = path.join(tmpDir, '.squad'); fs.mkdirSync(squadDir, { recursive: true }); fs.writeFileSync(path.join(squadDir, 'workstreams.json'), '{"defaultWorkflow":"direct"}', 'utf-8'); - expect(loadWorkstreamsConfig(tmpDir)).toBeNull(); + expect(loadSubSquadsConfig(tmpDir)).toBeNull(); }); it('defaults defaultWorkflow to branch-per-issue when missing', () => { const squadDir = path.join(tmpDir, '.squad'); fs.mkdirSync(squadDir, { recursive: true }); fs.writeFileSync(path.join(squadDir, 'workstreams.json'), '{"workstreams":[{"name":"a","labelFilter":"x"}]}', 'utf-8'); - const result = loadWorkstreamsConfig(tmpDir); + const result = loadSubSquadsConfig(tmpDir); expect(result!.defaultWorkflow).toBe('branch-per-issue'); }); it('preserves folderScope arrays', () => { writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); - const result = loadWorkstreamsConfig(tmpDir)!; + const result = loadSubSquadsConfig(tmpDir)!; expect(result.workstreams[0]!.folderScope).toEqual(['apps/web']); }); it('preserves optional description', () => { writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); - const result = loadWorkstreamsConfig(tmpDir)!; + const result = loadSubSquadsConfig(tmpDir)!; expect(result.workstreams[0]!.description).toBe('UI specialists'); expect(result.workstreams[1]!.description).toBeUndefined(); }); @@ -127,7 +137,7 @@ describe('loadWorkstreamsConfig', () => { // resolveStream // ============================================================================ -describe('resolveWorkstream', () => { +describe('resolveSubSquad', () => { let tmpDir: string; const origEnv = process.env.SQUAD_TEAM; @@ -149,7 +159,7 @@ describe('resolveWorkstream', () => { it('resolves from SQUAD_TEAM env var with matching config', () => { process.env.SQUAD_TEAM = 'ui-team'; writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); - const result = resolveWorkstream(tmpDir); + const result = resolveSubSquad(tmpDir); expect(result).not.toBeNull(); expect(result!.name).toBe('ui-team'); expect(result!.source).toBe('env'); @@ -159,17 +169,17 @@ describe('resolveWorkstream', () => { it('synthesizes definition from SQUAD_TEAM when no config exists', () => { process.env.SQUAD_TEAM = 'custom-team'; - const result = resolveWorkstream(tmpDir); + const result = resolveSubSquad(tmpDir); expect(result).not.toBeNull(); expect(result!.name).toBe('custom-team'); expect(result!.source).toBe('env'); expect(result!.definition.labelFilter).toBe('team:custom-team'); }); - it('synthesizes definition from SQUAD_TEAM when workstream not in config', () => { + it('synthesizes definition from SQUAD_TEAM when SubSquad not in config', () => { process.env.SQUAD_TEAM = 'unknown-team'; writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); - const result = resolveWorkstream(tmpDir); + const result = resolveSubSquad(tmpDir); expect(result).not.toBeNull(); expect(result!.name).toBe('unknown-team'); expect(result!.source).toBe('env'); @@ -181,7 +191,7 @@ describe('resolveWorkstream', () => { it('resolves from .squad-workstream file with matching config', () => { writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); writeSquadWorkstreamFile(tmpDir, 'backend-team'); - const result = resolveWorkstream(tmpDir); + const result = resolveSubSquad(tmpDir); expect(result).not.toBeNull(); expect(result!.name).toBe('backend-team'); expect(result!.source).toBe('file'); @@ -189,17 +199,17 @@ describe('resolveWorkstream', () => { }); it('synthesizes definition from .squad-workstream file when no config', () => { - writeSquadWorkstreamFile(tmpDir, 'my-workstream'); - const result = resolveWorkstream(tmpDir); + writeSquadWorkstreamFile(tmpDir, 'my-subsquad'); + const result = resolveSubSquad(tmpDir); expect(result).not.toBeNull(); - expect(result!.name).toBe('my-workstream'); + expect(result!.name).toBe('my-subsquad'); expect(result!.source).toBe('file'); - expect(result!.definition.labelFilter).toBe('team:my-workstream'); + expect(result!.definition.labelFilter).toBe('team:my-subsquad'); }); it('ignores empty .squad-workstream file', () => { fs.writeFileSync(path.join(tmpDir, '.squad-workstream'), ' \n', 'utf-8'); - expect(resolveWorkstream(tmpDir)).toBeNull(); + expect(resolveSubSquad(tmpDir)).toBeNull(); }); it('trims whitespace from .squad-workstream file', () => { @@ -210,9 +220,9 @@ describe('resolveWorkstream', () => { expect(result!.source).toBe('file'); }); - // --- Config resolution (single workstream auto-select) --- + // --- Config resolution (single SubSquad auto-select) --- - it('auto-selects single workstream from config', () => { + it('auto-selects single SubSquad from config', () => { const singleConfig: WorkstreamConfig = { workstreams: [{ name: 'solo', labelFilter: 'team:solo' }], defaultWorkflow: 'direct', @@ -226,11 +236,11 @@ describe('resolveWorkstream', () => { // --- Fallback --- - it('returns null when no workstream context exists', () => { + it('returns null when no SubSquad context exists', () => { expect(resolveWorkstream(tmpDir)).toBeNull(); }); - it('returns null when config has multiple workstreams but no env/file', () => { + it('returns null when config has multiple SubSquads but no env/file', () => { writeSquadWorkstreamsConfig(tmpDir, SAMPLE_CONFIG); expect(resolveWorkstream(tmpDir)).toBeNull(); }); @@ -262,138 +272,157 @@ describe('resolveWorkstream', () => { }); // ============================================================================ -// getStreamLabelFilter +// getSubSquadLabelFilter // ============================================================================ -describe('getWorkstreamLabelFilter', () => { +describe('getSubSquadLabelFilter', () => { it('returns the label filter from definition', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'ui-team', definition: { name: 'ui-team', labelFilter: 'team:ui' }, source: 'env', }; - expect(getWorkstreamLabelFilter(workstream)).toBe('team:ui'); + expect(getSubSquadLabelFilter(subsquad)).toBe('team:ui'); }); it('returns synthesized label filter', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'custom', definition: { name: 'custom', labelFilter: 'team:custom' }, source: 'file', }; - expect(getWorkstreamLabelFilter(workstream)).toBe('team:custom'); + expect(getSubSquadLabelFilter(subsquad)).toBe('team:custom'); + }); + + it('backward compat: getWorkstreamLabelFilter still works', () => { + const subsquad: ResolvedWorkstream = { + name: 'ui-team', + definition: { name: 'ui-team', labelFilter: 'team:ui' }, + source: 'env', + }; + expect(getWorkstreamLabelFilter(subsquad)).toBe('team:ui'); }); }); // ============================================================================ -// filterIssuesByStream +// filterIssuesBySubSquad // ============================================================================ -describe('filterIssuesByWorkstream', () => { - it('filters issues matching the workstream label', () => { - const workstream: ResolvedWorkstream = { +describe('filterIssuesBySubSquad', () => { + it('filters issues matching the SubSquad label', () => { + const subsquad: ResolvedSubSquad = { name: 'ui-team', definition: { name: 'ui-team', labelFilter: 'team:ui' }, source: 'env', }; - const result = filterIssuesByWorkstream(SAMPLE_ISSUES, workstream); + const result = filterIssuesBySubSquad(SAMPLE_ISSUES, subsquad); expect(result).toHaveLength(2); // issue 1 and 5 expect(result.map(i => i.number)).toEqual([1, 5]); }); it('returns empty array when no issues match', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'qa-team', definition: { name: 'qa-team', labelFilter: 'team:qa' }, source: 'env', }; - const result = filterIssuesByWorkstream(SAMPLE_ISSUES, workstream); + const result = filterIssuesBySubSquad(SAMPLE_ISSUES, subsquad); expect(result).toHaveLength(0); }); it('handles case-insensitive matching', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'ui-team', definition: { name: 'ui-team', labelFilter: 'TEAM:UI' }, source: 'env', }; - const result = filterIssuesByWorkstream(SAMPLE_ISSUES, workstream); + const result = filterIssuesBySubSquad(SAMPLE_ISSUES, subsquad); expect(result).toHaveLength(2); }); it('returns all issues when labelFilter is empty', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'all', definition: { name: 'all', labelFilter: '' }, source: 'env', }; - const result = filterIssuesByWorkstream(SAMPLE_ISSUES, workstream); + const result = filterIssuesBySubSquad(SAMPLE_ISSUES, subsquad); expect(result).toHaveLength(SAMPLE_ISSUES.length); }); it('handles issues with no labels', () => { - const issues: WorkstreamIssue[] = [ + const issues: SubSquadIssue[] = [ { number: 10, title: 'No labels', labels: [] }, ]; - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'ui-team', definition: { name: 'ui-team', labelFilter: 'team:ui' }, source: 'env', }; - expect(filterIssuesByWorkstream(issues, workstream)).toHaveLength(0); + expect(filterIssuesBySubSquad(issues, subsquad)).toHaveLength(0); }); it('handles empty issues array', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'ui-team', definition: { name: 'ui-team', labelFilter: 'team:ui' }, source: 'env', }; - expect(filterIssuesByWorkstream([], workstream)).toHaveLength(0); + expect(filterIssuesBySubSquad([], subsquad)).toHaveLength(0); }); it('filters backend-team correctly', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'backend-team', definition: { name: 'backend-team', labelFilter: 'team:backend' }, source: 'config', }; - const result = filterIssuesByWorkstream(SAMPLE_ISSUES, workstream); + const result = filterIssuesBySubSquad(SAMPLE_ISSUES, subsquad); expect(result).toHaveLength(2); // issue 2 and 5 expect(result.map(i => i.number)).toEqual([2, 5]); }); it('filters infra-team correctly (single match)', () => { - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'infra-team', definition: { name: 'infra-team', labelFilter: 'team:infra' }, source: 'file', }; - const result = filterIssuesByWorkstream(SAMPLE_ISSUES, workstream); + const result = filterIssuesBySubSquad(SAMPLE_ISSUES, subsquad); expect(result).toHaveLength(1); expect(result[0]!.number).toBe(3); }); + + it('backward compat: filterIssuesByWorkstream still works', () => { + const subsquad: ResolvedWorkstream = { + name: 'ui-team', + definition: { name: 'ui-team', labelFilter: 'team:ui' }, + source: 'env', + }; + const result = filterIssuesByWorkstream(SAMPLE_ISSUES, subsquad); + expect(result).toHaveLength(2); + }); }); // ============================================================================ // Type checks (compile-time — these just verify the types work) // ============================================================================ -describe('Workstream types', () => { - it('WorkstreamDefinition accepts all fields', () => { - const def: WorkstreamDefinition = { +describe('SubSquad types', () => { + it('SubSquadDefinition accepts all fields', () => { + const def: SubSquadDefinition = { name: 'test', labelFilter: 'team:test', folderScope: ['src/'], workflow: 'branch-per-issue', - description: 'Test workstream', + description: 'Test SubSquad', }; expect(def.name).toBe('test'); expect(def.workflow).toBe('branch-per-issue'); }); - it('WorkstreamDefinition works with minimal fields', () => { - const def: WorkstreamDefinition = { + it('SubSquadDefinition works with minimal fields', () => { + const def: SubSquadDefinition = { name: 'minimal', labelFilter: 'team:minimal', }; @@ -402,8 +431,8 @@ describe('Workstream types', () => { expect(def.description).toBeUndefined(); }); - it('WorkstreamConfig has required fields', () => { - const config: WorkstreamConfig = { + it('SubSquadConfig has required fields', () => { + const config: SubSquadConfig = { workstreams: [], defaultWorkflow: 'direct', }; @@ -411,8 +440,8 @@ describe('Workstream types', () => { expect(config.defaultWorkflow).toBe('direct'); }); - it('ResolvedWorkstream has source provenance', () => { - const resolved: ResolvedWorkstream = { + it('ResolvedSubSquad has source provenance', () => { + const resolved: ResolvedSubSquad = { name: 'test', definition: { name: 'test', labelFilter: 'x' }, source: 'env', @@ -425,7 +454,7 @@ describe('Workstream types', () => { // Init integration (streams.json generation) // ============================================================================ -describe('initSquad with workstreams', () => { +describe('initSquad with SubSquads', () => { let tmpDir: string; beforeEach(() => { tmpDir = makeTmpDir(); }); @@ -433,7 +462,7 @@ describe('initSquad with workstreams', () => { it('generates workstreams.json when streams option is provided', async () => { const { initSquad } = await import('../packages/squad-sdk/src/config/init.js'); - const streams: WorkstreamDefinition[] = [ + const streams: SubSquadDefinition[] = [ { name: 'ui-team', labelFilter: 'team:ui', folderScope: ['apps/web'] }, { name: 'api-team', labelFilter: 'team:api' }, ]; @@ -451,7 +480,7 @@ describe('initSquad with workstreams', () => { const workstreamsPath = path.join(tmpDir, '.squad', 'workstreams.json'); expect(fs.existsSync(workstreamsPath)).toBe(true); - const content = JSON.parse(fs.readFileSync(workstreamsPath, 'utf-8')) as WorkstreamConfig; + const content = JSON.parse(fs.readFileSync(workstreamsPath, 'utf-8')) as SubSquadConfig; expect(content.workstreams).toHaveLength(2); expect(content.workstreams[0]!.name).toBe('ui-team'); expect(content.defaultWorkflow).toBe('branch-per-issue'); @@ -462,7 +491,7 @@ describe('initSquad with workstreams', () => { await initSquad({ teamRoot: tmpDir, - projectName: 'test-no-workstreams', + projectName: 'test-no-subsquads', agents: [{ name: 'lead', role: 'lead' }], includeWorkflows: false, includeTemplates: false, @@ -502,11 +531,11 @@ describe('CLI activate behavior', () => { beforeEach(() => { tmpDir = makeTmpDir(); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it('writes .squad-workstream file with the workstream name', () => { + it('writes .squad-workstream file with the SubSquad name', () => { const filePath = path.join(tmpDir, '.squad-workstream'); - fs.writeFileSync(filePath, 'my-workstream\n', 'utf-8'); + fs.writeFileSync(filePath, 'my-subsquad\n', 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8').trim(); - expect(content).toBe('my-workstream'); + expect(content).toBe('my-subsquad'); }); it('resolves after activation', () => { @@ -549,44 +578,44 @@ describe('Edge cases', () => { } }); - it('handles empty workstreams array in config', () => { - const emptyConfig: WorkstreamConfig = { workstreams: [], defaultWorkflow: 'direct' }; + it('handles empty SubSquads array in config', () => { + const emptyConfig: SubSquadConfig = { workstreams: [], defaultWorkflow: 'direct' }; writeSquadWorkstreamsConfig(tmpDir, emptyConfig); - expect(resolveWorkstream(tmpDir)).toBeNull(); + expect(resolveSubSquad(tmpDir)).toBeNull(); }); it('handles config with workstreams but non-array type', () => { const squadDir = path.join(tmpDir, '.squad'); fs.mkdirSync(squadDir, { recursive: true }); fs.writeFileSync(path.join(squadDir, 'workstreams.json'), '{"workstreams":"not-array"}', 'utf-8'); - expect(loadWorkstreamsConfig(tmpDir)).toBeNull(); + expect(loadSubSquadsConfig(tmpDir)).toBeNull(); }); it('handles SQUAD_TEAM set to empty string', () => { process.env.SQUAD_TEAM = ''; - expect(resolveWorkstream(tmpDir)).toBeNull(); + expect(resolveSubSquad(tmpDir)).toBeNull(); }); - it('filterIssuesByWorkstream handles labels with special characters', () => { - const issues: WorkstreamIssue[] = [ + it('filterIssuesBySubSquad handles labels with special characters', () => { + const issues: SubSquadIssue[] = [ { number: 1, title: 'Test', labels: [{ name: 'team:front-end/ui' }] }, ]; - const workstream: ResolvedWorkstream = { + const subsquad: ResolvedSubSquad = { name: 'fe', definition: { name: 'fe', labelFilter: 'team:front-end/ui' }, source: 'env', }; - const result = filterIssuesByWorkstream(issues, workstream); + const result = filterIssuesBySubSquad(issues, subsquad); expect(result).toHaveLength(1); }); it('resolves workflow from definition over defaultWorkflow', () => { - const config: WorkstreamConfig = { - workstreams: [{ name: 'direct-workstream', labelFilter: 'team:direct', workflow: 'direct' }], + const config: SubSquadConfig = { + workstreams: [{ name: 'direct-subsquad', labelFilter: 'team:direct', workflow: 'direct' }], defaultWorkflow: 'branch-per-issue', }; writeSquadWorkstreamsConfig(tmpDir, config); - const result = resolveWorkstream(tmpDir); + const result = resolveSubSquad(tmpDir); expect(result!.definition.workflow).toBe('direct'); }); });