feat: Squad Workstreams — horizontal scaling via Codespaces#189
feat: Squad Workstreams — horizontal scaling via Codespaces#189tamirdresher wants to merge 0 commit intobradygaster:mainfrom
Conversation
CLI functions now throw errors or return results instead of calling process.exit(). fatal() throws SquadError instead of process.exit(1). runWatch() uses Promise resolution for graceful shutdown. runShell() closes readline on SIGINT instead of process.exit(). process.exit() is confined to the CLI entry point only. VS Code extensions can safely import these functions. Closes bradygaster#189 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on merge Session: 2026-02-22T020714Z-epic181-complete Orchestration: 5 agents (Fenster, Edie, Kujan, Hockney, McManus) Changes: - Created 5 orchestration-log entries per agent spawn - Created session log documenting epic completion and all closed issues - Merged 4 inbox decisions into decisions.md (CRLF, CLI entry split, process.exit refactor, docs as you go) - Normalized decision formatting (consistent ### heading style) - Propagated team updates to affected agent history.md files (Fenster, Edie, Kujan, Hockney, McManus) - Deleted 4 inbox files after merge Issues closed: bradygaster#220, bradygaster#221, bradygaster#187, bradygaster#189, bradygaster#228, bradygaster#181 Tests passing: 1683/1683 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a first-class Streams concept to Squad to support multi-Codespace / horizontally scaled workflows by scoping issue triage and local activation to a named stream.
Changes:
- Introduces an SDK
streamsmodule (types + resolver + issue filtering) and exports it from the SDK. - Adds a new CLI command group:
squad streams list|status|activate. - Adds init support for generating
.squad/streams.json, updates templates/docs, and adds a comprehensive streams test suite.
Reviewed changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| test/streams.test.ts | New vitest suite covering streams config loading, resolution, filtering, and init integration. |
| templates/squad.agent.md | Adds “Stream Awareness” coordinator guidance for env-based stream context and scoping. |
| packages/squad-sdk/src/types.ts | Re-exports stream-related public types from SDK types barrel. |
| packages/squad-sdk/src/streams/types.ts | Defines StreamDefinition, StreamConfig, and ResolvedStream types. |
| packages/squad-sdk/src/streams/resolver.ts | Implements loadStreamsConfig() + resolveStream() + getStreamLabelFilter(). |
| packages/squad-sdk/src/streams/index.ts | Streams barrel export. |
| packages/squad-sdk/src/streams/filter.ts | Implements filterIssuesByStream() helper. |
| packages/squad-sdk/src/index.ts | Exports the new streams module from the SDK root. |
| packages/squad-sdk/src/config/init.ts | Adds optional streams config generation + .squad-stream gitignore entry during init. |
| packages/squad-sdk/package.json | Bumps SDK version to 0.8.18-preview.2. |
| packages/squad-cli/src/cli/commands/streams.ts | New squad streams subcommands (list/status/activate). |
| packages/squad-cli/src/cli-entry.ts | Wires the streams command into the CLI entry + help output. |
| packages/squad-cli/package.json | Bumps CLI version to 0.8.18-preview.2. |
| package.json | Bumps workspace version to 0.8.18-preview.2. |
| package-lock.json | Updates lockfile (deps + copilot/copilot-sdk versions), but versions are out of sync with package.json. |
| docs/specs/streams-prd.md | Adds a Streams PRD documenting requirements and design decisions. |
| docs/scenarios/multi-codespace.md | Adds a multi-Codespace walkthrough for running multiple streams in parallel. |
| docs/features/streams.md | Adds user-facing feature documentation for configuration, activation, and CLI usage. |
| .gitignore | Adds .squad-stream to ignores as a local activation file. |
| * 3. squad.config.ts → streams.active field (via .squad/streams.json) | ||
| * 4. null (no stream — single-squad mode) |
There was a problem hiding this comment.
The resolver docstring claims a squad.config.ts → streams.active resolution step, but resolveStream() doesn’t read squad.config.ts or any active field. Please update the comment to reflect the actual behavior (Env → .squad-stream → single-stream auto-select → null), or implement the documented config-based activation.
| * 3. squad.config.ts → streams.active field (via .squad/streams.json) | |
| * 4. null (no stream — single-squad mode) | |
| * 3. If exactly one stream is defined in the config, auto-select that stream | |
| * 4. null (no active stream — single-squad mode / no streams) |
| const parsed = JSON.parse(raw) as StreamConfig; | ||
|
|
||
| // Basic validation | ||
| if (!parsed.streams || !Array.isArray(parsed.streams)) { | ||
| return null; | ||
| } | ||
|
|
||
| // Ensure defaultWorkflow has a value | ||
| if (!parsed.defaultWorkflow) { | ||
| parsed.defaultWorkflow = 'branch-per-issue'; | ||
| } | ||
|
|
||
| return parsed; |
There was a problem hiding this comment.
loadStreamsConfig() only validates that streams is an array, but it doesn’t validate each entry’s shape (e.g., name/labelFilter are strings, folderScope is string[], workflow is an allowed value) or that defaultWorkflow is one of the supported modes. Since other code assumes these fields exist and are well-typed, consider doing stricter validation (return null / filter invalid entries) and normalizing defaults without relying on a type cast.
| const parsed = JSON.parse(raw) as StreamConfig; | |
| // Basic validation | |
| if (!parsed.streams || !Array.isArray(parsed.streams)) { | |
| return null; | |
| } | |
| // Ensure defaultWorkflow has a value | |
| if (!parsed.defaultWorkflow) { | |
| parsed.defaultWorkflow = 'branch-per-issue'; | |
| } | |
| return parsed; | |
| const rawConfig = JSON.parse(raw) as unknown; | |
| if (!rawConfig || typeof rawConfig !== 'object') { | |
| return null; | |
| } | |
| const configLike = rawConfig as { defaultWorkflow?: unknown; streams?: unknown }; | |
| // Derive a sane defaultWorkflow value | |
| const defaultWorkflow = | |
| typeof configLike.defaultWorkflow === 'string' && configLike.defaultWorkflow.trim() !== '' | |
| ? configLike.defaultWorkflow | |
| : 'branch-per-issue'; | |
| const streamsRaw = configLike.streams; | |
| if (!Array.isArray(streamsRaw)) { | |
| return null; | |
| } | |
| const streams: StreamDefinition[] = streamsRaw | |
| .filter(entry => entry && typeof entry === 'object') | |
| .map(entry => { | |
| const e = entry as { | |
| name?: unknown; | |
| labelFilter?: unknown; | |
| folderScope?: unknown; | |
| workflow?: unknown; | |
| }; | |
| if (typeof e.name !== 'string' || typeof e.labelFilter !== 'string') { | |
| return null; | |
| } | |
| const normalized: any = { | |
| name: e.name, | |
| labelFilter: e.labelFilter, | |
| }; | |
| if (Array.isArray(e.folderScope) && e.folderScope.every(item => typeof item === 'string')) { | |
| normalized.folderScope = e.folderScope; | |
| } | |
| if (typeof e.workflow === 'string' && e.workflow.trim() !== '') { | |
| normalized.workflow = e.workflow; | |
| } else { | |
| normalized.workflow = defaultWorkflow; | |
| } | |
| return normalized as StreamDefinition; | |
| }) | |
| .filter((s): s is StreamDefinition => s !== null); | |
| if (streams.length === 0) { | |
| return null; | |
| } | |
| const normalizedConfig: StreamConfig = { | |
| defaultWorkflow, | |
| streams, | |
| }; | |
| return normalizedConfig; |
| const block = (currentIgnore && !currentIgnore.endsWith('\n') ? '\n' : '') | ||
| + '# Squad: stream activation file (local to this machine)\n' | ||
| + streamIgnoreEntry + '\n'; | ||
| await appendFile(gitignorePath, block); |
There was a problem hiding this comment.
When appending the .squad-stream entry to .gitignore, this block doesn’t add the path to createdFiles (unlike the earlier .gitignore update logic above). This means squad init won’t report that it modified .gitignore. Consider recording the change consistently (and/or reusing the existing ignore-entry helper logic).
| await appendFile(gitignorePath, block); | |
| await appendFile(gitignorePath, block); | |
| createdFiles.push(toRelativePath(gitignorePath)); |
There was a problem hiding this comment.
Already addressed — line 878 has \createdFiles.push(toRelativePath(gitignorePath)).
| On session start, check for stream context: | ||
| 1. Read `SQUAD_TEAM` env var | ||
| 2. If set, read `.squad/streams.json` and find matching stream | ||
| 3. Apply the stream's `labelFilter` — Ralph should ONLY pick up issues matching this label | ||
| 4. Apply the stream's `workflow` — if `branch-per-issue`, enforce creating a branch and PR for every issue (never commit directly to main) | ||
| 5. Apply `folderScope` — agents should only modify files in these directories | ||
|
|
||
| If no stream is detected, operate in default single-squad mode. |
There was a problem hiding this comment.
This “Stream Awareness” section only checks SQUAD_TEAM and doesn’t mention .squad-stream file activation or single-stream auto-select, which are part of the implemented resolver behavior. Also, it states agents should “only modify files” in folderScope, but the Streams docs/PRD describe folderScope as advisory rather than a hard lock—please align this guidance with the intended behavior.
| On session start, check for stream context: | |
| 1. Read `SQUAD_TEAM` env var | |
| 2. If set, read `.squad/streams.json` and find matching stream | |
| 3. Apply the stream's `labelFilter` — Ralph should ONLY pick up issues matching this label | |
| 4. Apply the stream's `workflow` — if `branch-per-issue`, enforce creating a branch and PR for every issue (never commit directly to main) | |
| 5. Apply `folderScope` — agents should only modify files in these directories | |
| If no stream is detected, operate in default single-squad mode. | |
| On session start, resolve stream context using the Streams resolver: | |
| 1. Check for a `.squad-stream` file in the repo or working directory. If present, activate the referenced stream. | |
| 2. If no `.squad-stream` is present, read the `SQUAD_TEAM` env var (if set) and resolve the matching stream from `.squad/streams.json`. | |
| 3. If there is exactly one stream defined in `.squad/streams.json` and nothing else selects a stream, auto-select that stream. | |
| 4. When a stream is active: | |
| - Apply the stream's `labelFilter` — Ralph should normally only pick up issues matching this label unless the user explicitly directs otherwise. | |
| - Apply the stream's `workflow` — if `branch-per-issue`, enforce creating a branch and PR for every issue (never commit directly to main). | |
| - Apply the stream'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 stream is resolved, operate in default single-squad mode. |
| // Try to get branch info | ||
| try { | ||
| const branchOutput = execSync( | ||
| `git branch --list "*${stream.name}*"`, | ||
| { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, | ||
| ); |
There was a problem hiding this comment.
execSync() is invoked with a shell command string that includes stream.name from configuration. This can be exploited for shell injection and can also behave unexpectedly with glob/special characters. Prefer execFileSync/spawnSync with argv (e.g., git, ['branch','--list',pattern]) and avoid invoking a shell.
There was a problem hiding this comment.
Already addressed — the code uses \spawnSync('git', ['branch', '--list', pattern])\ with args array (line 131-134). No \�xecSync\ in streams.ts.
| export async function runStreams(cwd: string, args: string[]): Promise<void> { | ||
| const sub = args[0]; | ||
|
|
||
| if (!sub || sub === 'list') { | ||
| return listStreams(cwd); | ||
| } | ||
| if (sub === 'status') { | ||
| return showStreamStatus(cwd); | ||
| } | ||
| if (sub === 'activate') { | ||
| const name = args[1]; | ||
| if (!name) { | ||
| console.error(`${RED}✗${RESET} Usage: squad streams activate <name>`); | ||
| process.exit(1); | ||
| } | ||
| return activateStream(cwd, name); | ||
| } | ||
|
|
||
| console.error(`${RED}✗${RESET} Unknown streams subcommand: ${sub}`); | ||
| console.log(`\nUsage: squad streams <list|status|activate <name>>`); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
This new streams CLI surface isn’t covered by automated tests. The existing suite has extensive CLI coverage, but there are no tests exercising runStreams/subcommands (and the new test/streams.test.ts only writes .squad-stream directly). Add tests that invoke runStreams (and assert output / file creation / error paths) to prevent regressions.
| * Stream-Aware Issue Filtering | ||
| * | ||
| * Filters GitHub issues to only those matching a stream's labelFilter. | ||
| * Used by Ralph during triage to scope work to the active stream. |
There was a problem hiding this comment.
The module comment says this filter is “Used by Ralph during triage”, but there’s currently no usage of filterIssuesByStream() anywhere in the SDK/CLI codebase. Either wire this into the triage path (so the comment is true) or adjust the comment to avoid implying behavior that isn’t implemented yet.
| * Used by Ralph during triage to scope work to the active stream. | |
| * Intended to scope work to the active stream during triage. |
| extractionDisabled?: boolean; | ||
| /** Optional stream definitions — generates .squad/streams.json when provided */ | ||
| streams?: StreamDefinition[]; |
There was a problem hiding this comment.
PR description mentions an init --stream flag that generates streams.json, but the only implementation here is a programmatic InitOptions.streams hook; there’s no CLI parsing for --stream in the repo. Either add the CLI flag wiring or adjust the PR description/docs so they match what’s actually shipped.
| try { | ||
| const prOutput = execSync( | ||
| `gh pr list --label "${stream.labelFilter}" --json number,title,state --limit 5`, | ||
| { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, | ||
| ); |
There was a problem hiding this comment.
execSync() is invoked with a shell command string that includes stream.labelFilter from configuration. Because this value is user-controlled, this is vulnerable to shell injection (and can also break when the label contains quotes). Prefer execFileSync/spawnSync with an argv array (no shell) and treat labelFilter as a plain argument.
There was a problem hiding this comment.
Already addressed — the code uses \spawnSync('gh', ['pr', 'list', '--label', workstream.labelFilter, ...])\ with args array (line 110-113). No \�xecSync\ calls exist in streams.ts.
f16729e to
b069cda
Compare
Session: 2026-03-05T21:05Z-release-planning Requested by: Copilot (Scribe role) Changes: - Logged orchestration entries for Keaton, McManus, Kobayashi, Fenster, Coordinator - Created session log documenting v0.8.21 release planning outcomes - Merged 3 decision inbox files into decisions.md - Deleted inbox files (contributor page, PR merges, user directives) Decision Merged: - Every release MUST include contributor page update - Workstreams MUST be included in v0.8.21 Outcomes documented: - 4 PRs merged to dev (#204, #203, #198, #189) - 2 issues fixed (#210, #195) - Build passing, 98.8% test coverage - Release scope: 18 unreleased commits
… fix Session: 2026-03-07T16-19-00Z-pre-release-triage Requested by: Brady (Team Coordinator) Changes: - Merged decisions from 3 agent triage sessions (Keaton, Hockney, McManus) - Brady directives: SDK-First v0.8.22 commitment, Actions-to-CLI strategic shift - Updated agent history.md with cross-team context propagation - Decisions logged: v0.8.21 release gate, PR holds for v0.8.22, docs readiness Results: - v0.8.21: GREEN LIGHT (pending #248 fix per Keaton override) - v0.8.22 roadmap: 9 issues, 3 parallel streams - Close: #194 (completed), #231 (duplicate) - PRs #189/#191: Hold for v0.8.22 (rebase to dev) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 26 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
test/acceptance/features/status-extended.feature:16
- This scenario expects
squad statusto exit with code 1 when no .squad directory exists, but the CLI implementation returns normally (exit code 0) in that case and other acceptance tests still assert exit code 0. Please align this expectation with the actual command behavior (or update the command consistently if you intendstatusto be a failing gate).
Scenario: Status in directory without squad shows no active squad
Given a directory without a ".squad" directory
When I run "squad status" in the temp directory
Then the output does not contain "Active squad: repo"
And the exit code is 1
| 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. |
There was a problem hiding this comment.
The documented workstream resolution priority here (file before env var) conflicts with the actual resolver implementation and other docs/tests, which resolve SQUAD_TEAM before .squad-workstream. Please update this section so the documented order matches the code (env > file > single-config).
| 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. | |
| 1. Read the `SQUAD_TEAM` env var (if set) and resolve the matching workstream from `.squad/workstreams.json`. | |
| 2. If no `SQUAD_TEAM` is set, check for a `.squad-workstream` file in the repo root. If present, activate the referenced workstream. | |
| 3. If there is exactly one workstream defined in `.squad/workstreams.json` and neither the env var nor the file selects a workstream, auto-select it. |
| - [ ] **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 |
There was a problem hiding this comment.
The requirements list says "Agents restrict modifications to workstream's folder scope", but later in the PRD (and in the implementation) folderScope is explicitly advisory, not enforced. Please update this requirement wording to match the intended advisory behavior to avoid a self-contradicting spec.
| - [ ] **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 | |
| - [ ] **Folder Scoping**: Agents treat the workstream's folder scope as advisory guidance for where to focus modifications | |
| - [ ] **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, including advisory folder scoping |
| if (typeof e.workflow === 'string' && e.workflow.trim() !== '') { | ||
| normalized.workflow = e.workflow; |
There was a problem hiding this comment.
loadWorkstreamsConfig accepts any non-empty string as workflow and passes it through, even though the WorkstreamDefinition type restricts workflow to 'branch-per-issue' | 'direct'. This can yield runtime configs with invalid workflow values. Validate e.workflow against the allowed set (and fallback to defaultWorkflow or drop the entry) instead of accepting arbitrary strings.
| if (typeof e.workflow === 'string' && e.workflow.trim() !== '') { | |
| normalized.workflow = e.workflow; | |
| if (typeof e.workflow === 'string') { | |
| const wf = e.workflow.trim(); | |
| if (validWorkflows.includes(wf as (typeof validWorkflows)[number])) { | |
| normalized.workflow = wf; | |
| } else { | |
| normalized.workflow = defaultWorkflow; | |
| } |
| issues: WorkstreamIssue[], | ||
| workstream: ResolvedWorkstream, | ||
| ): WorkstreamIssue[] { | ||
| const filter = workstream.definition.labelFilter; |
There was a problem hiding this comment.
filterIssuesByWorkstream treats a whitespace-only labelFilter (e.g. " ") as a real filter, which will then match nothing. Consider trimming the labelFilter and treating empty/whitespace-only as "no filter" (passthrough) to match the documented behavior for an "empty" filter.
| const filter = workstream.definition.labelFilter; | |
| const rawFilter = workstream.definition.labelFilter; | |
| if (!rawFilter) { | |
| return issues; | |
| } | |
| const filter = rawFilter.trim(); |
| try { | ||
| const result = spawnSync( | ||
| 'gh', | ||
| ['pr', 'list', '--label', workstream.labelFilter, '--json', 'number,title,state', '--limit', '5'], | ||
| { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, | ||
| ); | ||
| const prOutput = result.stdout ?? ''; | ||
| const prs = JSON.parse(prOutput) as Array<{ number: number; title: string; state: string }>; | ||
| if (prs.length > 0) { | ||
| console.log(` ${YELLOW}PRs:${RESET}`); | ||
| for (const pr of prs) { | ||
| console.log(` #${pr.number} ${pr.title} (${pr.state})`); | ||
| } | ||
| } else { | ||
| console.log(` ${DIM}No open PRs${RESET}`); | ||
| } | ||
| } catch { | ||
| console.log(` ${DIM}(gh CLI not available — skipping PR lookup)${RESET}`); |
There was a problem hiding this comment.
If gh pr list --json ... fails (auth error, not a git repo, rate limit, etc.) or returns non-JSON on stderr, JSON.parse will throw and the catch message claims the gh CLI is unavailable. Consider checking spawnSync's status/error and reporting a more accurate message (and/or printing stderr) so users can diagnose gh-related failures.
| try { | |
| const result = spawnSync( | |
| 'gh', | |
| ['pr', 'list', '--label', workstream.labelFilter, '--json', 'number,title,state', '--limit', '5'], | |
| { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, | |
| ); | |
| const prOutput = result.stdout ?? ''; | |
| const prs = JSON.parse(prOutput) as Array<{ number: number; title: string; state: string }>; | |
| if (prs.length > 0) { | |
| console.log(` ${YELLOW}PRs:${RESET}`); | |
| for (const pr of prs) { | |
| console.log(` #${pr.number} ${pr.title} (${pr.state})`); | |
| } | |
| } else { | |
| console.log(` ${DIM}No open PRs${RESET}`); | |
| } | |
| } catch { | |
| console.log(` ${DIM}(gh CLI not available — skipping PR lookup)${RESET}`); | |
| const result = spawnSync( | |
| 'gh', | |
| ['pr', 'list', '--label', workstream.labelFilter, '--json', 'number,title,state', '--limit', '5'], | |
| { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, | |
| ); | |
| if (result.error) { | |
| console.log(` ${DIM}(failed to run gh CLI: ${result.error.message} — skipping PR lookup)${RESET}`); | |
| } else if (result.status !== 0) { | |
| const stderr = (result.stderr ?? '').toString().trim(); | |
| console.log(` ${DIM}(gh CLI returned an error — skipping PR lookup)${RESET}`); | |
| if (stderr) { | |
| console.log(` ${DIM}gh stderr:${RESET} ${stderr}`); | |
| } | |
| } else { | |
| const prOutput = (result.stdout ?? '').toString(); | |
| try { | |
| const prs = JSON.parse(prOutput) as Array<{ number: number; title: string; state: string }>; | |
| if (prs.length > 0) { | |
| console.log(` ${YELLOW}PRs:${RESET}`); | |
| for (const pr of prs) { | |
| console.log(` #${pr.number} ${pr.title} (${pr.state})`); | |
| } | |
| } else { | |
| console.log(` ${DIM}No open PRs${RESET}`); | |
| } | |
| } catch { | |
| console.log(` ${DIM}(failed to parse gh CLI output — skipping PR lookup)${RESET}`); | |
| } |
| 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'; |
There was a problem hiding this comment.
WorkstreamDefinition is imported as a type here but never used. Please remove the unused import to keep the module clean.
| import type { WorkstreamDefinition } from '@bradygaster/squad-sdk'; |
| |-------|----------|-------------| | ||
| | `name` | Yes | Unique workstream identifier (kebab-case) | | ||
| | `labelFilter` | Yes | GitHub label to filter issues | | ||
| | `folderScope` | No | Directories this workstream may modify | |
There was a problem hiding this comment.
This table describes folderScope as directories the workstream "may modify", but the section below states folderScope is advisory (not enforced). Please align the table wording with the advisory semantics to avoid implying hard restrictions.
| | `folderScope` | No | Directories this workstream may modify | | |
| | `folderScope` | No | Preferred directories for this workstream’s changes (advisory, not enforced) | |
|
|
||
|
|
||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
There are multiple trailing blank lines at the end of the file after the main().catch block. Please remove them to avoid unnecessary diff noise and keep formatting consistent.
e4c0fb5 to
a41bfd0
Compare
Squad Workstreams — Horizontal Scaling via Codespaces
PRD: #200
Problem
Squad currently runs as a single team per repo. When scaling to multiple Codespaces (each running its own Squad instance), there's no built-in way to scope each instance to a subset of issues, enforce branch+PR workflow, or monitor workstreams centrally.
Evidence
Validated via a multi-Codespace experiment with tamirdresher/squad-tetris — 3 Codespaces building a multiplayer Tetris game, each scoped to team:ui/team:backend/team:cloud issues. Findings:
Solution
Add
workstreamsas a first-class concept:SDK (
packages/squad-sdk/src/streams/)WorkstreamDefinitiontype: name, labelFilter, folderScope (advisory), workflowresolveWorkstream()— auto-detect fromSQUAD_TEAMenv var,.squad-workstreamfile, or single-workstream auto-selectfilterIssuesByWorkstream()— filter issues by workstream's labelloadWorkstreamsConfig()— parse and validate workstreams.json with strict entry validationworkstreams.jsonCLI (
packages/squad-cli/src/cli/commands/streams.ts)squad workstreams list— show configured workstreamssquad workstreams status— show per-workstream activity (branches, PRs)squad workstreams activate <name>— write.squad-workstreamfilesquad streamsalias still worksCoordinator (
templates/squad.agent.md)Tests: 44 new tests covering resolution, filtering, config validation, CLI behavior
Docs: Feature guide, multi-Codespace scenario walkthrough, PRD with experiment findings
Key Design Decisions
squad workstreams activateallows sequential testing on one machinestreamsCLI alias preservedCo-authored-by: Copilot 223556219+Copilot@users.noreply.github.com