Skip to content

feat: Squad Workstreams — horizontal scaling via Codespaces#189

Closed
tamirdresher wants to merge 0 commit intobradygaster:mainfrom
tamirdresher:feature/squad-streams
Closed

feat: Squad Workstreams — horizontal scaling via Codespaces#189
tamirdresher wants to merge 0 commit intobradygaster:mainfrom
tamirdresher:feature/squad-streams

Conversation

@tamirdresher
Copy link
Copy Markdown
Collaborator

@tamirdresher tamirdresher commented Mar 4, 2026

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:

  • Backend squad pushed a branch but no PR (no workflow enforcement)
  • Other squads didn't push branches (no branch-per-issue discipline)
  • Manual verbal directives needed per Codespace to set scope
  • No cross-workstream monitoring

Solution

Add workstreams as a first-class concept:

SDK (packages/squad-sdk/src/streams/)

  • WorkstreamDefinition type: name, labelFilter, folderScope (advisory), workflow
  • resolveWorkstream() — auto-detect from SQUAD_TEAM env var, .squad-workstream file, or single-workstream auto-select
  • filterIssuesByWorkstream() — filter issues by workstream's label
  • loadWorkstreamsConfig() — parse and validate workstreams.json with strict entry validation
  • Init support generates workstreams.json

CLI (packages/squad-cli/src/cli/commands/streams.ts)

  • squad workstreams list — show configured workstreams
  • squad workstreams status — show per-workstream activity (branches, PRs)
  • squad workstreams activate <name> — write .squad-workstream file
  • Backward compat: squad streams alias still works

Coordinator (templates/squad.agent.md)

  • Workstream Awareness section: auto-detect, enforce branch+PR workflow, advisory folderScope

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

  • folderScope is advisory — not a hard lock. Agents prefer these directories but can modify shared code with callout
  • Single-machine multi-workstreamsquad workstreams activate allows sequential testing on one machine
  • spawnSync over execSync — prevents shell injection from user-controlled config values
  • Strict config validation — each workstream entry validated for shape, invalid entries filtered out
  • Backward compatible — repos without workstreams.json work exactly as before; streams CLI alias preserved

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

williamhallatt pushed a commit to williamhallatt/squad that referenced this pull request Mar 4, 2026
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>
williamhallatt pushed a commit to williamhallatt/squad that referenced this pull request Mar 4, 2026
…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>
@bradygaster bradygaster requested a review from Copilot March 4, 2026 23:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 streams module (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.

Comment on lines +7 to +8
* 3. squad.config.ts → streams.active field (via .squad/streams.json)
* 4. null (no stream — single-squad mode)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* 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)

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +43
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;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
const block = (currentIgnore && !currentIgnore.endsWith('\n') ? '\n' : '')
+ '# Squad: stream activation file (local to this machine)\n'
+ streamIgnoreEntry + '\n';
await appendFile(gitignorePath, block);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
await appendFile(gitignorePath, block);
await appendFile(gitignorePath, block);
createdFiles.push(toRelativePath(gitignorePath));

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed — line 878 has \createdFiles.push(toRelativePath(gitignorePath)).

Comment thread templates/squad.agent.md Outdated
Comment on lines +116 to +123
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.
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +129
// Try to get branch info
try {
const branchOutput = execSync(
`git branch --list "*${stream.name}*"`,
{ cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed — the code uses \spawnSync('git', ['branch', '--list', pattern])\ with args array (line 131-134). No \�xecSync\ in streams.ts.

Comment on lines +26 to +47
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);
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
* 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.
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* Used by Ralph during triage to scope work to the active stream.
* Intended to scope work to the active stream during triage.

Copilot uses AI. Check for mistakes.
Comment thread packages/squad-sdk/src/config/init.ts Outdated
Comment on lines +112 to +114
extractionDisabled?: boolean;
/** Optional stream definitions — generates .squad/streams.json when provided */
streams?: StreamDefinition[];
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +110
try {
const prOutput = execSync(
`gh pr list --label "${stream.labelFilter}" --json number,title,state --limit 5`,
{ cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tamirdresher tamirdresher force-pushed the feature/squad-streams branch 2 times, most recently from f16729e to b069cda Compare March 5, 2026 06:48
@tamirdresher tamirdresher changed the title feat: Squad Streams — horizontal scaling via Codespaces feat: Squad Workstreams — horizontal scaling via Codespaces Mar 5, 2026
bradygaster added a commit that referenced this pull request Mar 5, 2026
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
bradygaster added a commit that referenced this pull request Mar 7, 2026
Reviewed PR #189 (Workstreams) and PR #191 (ADO Adapter).
Both held for v0.8.22 — merge conflicts, no CI, missing tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bradygaster added a commit that referenced this pull request Mar 7, 2026
… 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>
@bradygaster bradygaster requested a review from Copilot March 8, 2026 13:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 status to 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 intend status to 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

Comment thread templates/squad.agent.md
Comment on lines +117 to +119
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.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment thread docs/specs/streams-prd.md
Comment on lines +25 to +28
- [ ] **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
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- [ ] **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

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +80
if (typeof e.workflow === 'string' && e.workflow.trim() !== '') {
normalized.workflow = e.workflow;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
issues: WorkstreamIssue[],
workstream: ResolvedWorkstream,
): WorkstreamIssue[] {
const filter = workstream.definition.labelFilter;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const filter = workstream.definition.labelFilter;
const rawFilter = workstream.definition.labelFilter;
if (!rawFilter) {
return issues;
}
const filter = rawFilter.trim();

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +126
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}`);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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}`);
}

Copilot uses AI. Check for mistakes.
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';
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkstreamDefinition is imported as a type here but never used. Please remove the unused import to keep the module clean.

Suggested change
import type { WorkstreamDefinition } from '@bradygaster/squad-sdk';

Copilot uses AI. Check for mistakes.
Comment thread docs/features/streams.md
|-------|----------|-------------|
| `name` | Yes | Unique workstream identifier (kebab-case) |
| `labelFilter` | Yes | GitHub label to filter issues |
| `folderScope` | No | Directories this workstream may modify |
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
| `folderScope` | No | Directories this workstream may modify |
| `folderScope` | No | Preferred directories for this workstream’s changes (advisory, not enforced) |

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +298





Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change

Copilot uses AI. Check for mistakes.
@tamirdresher tamirdresher force-pushed the feature/squad-streams branch from e4c0fb5 to a41bfd0 Compare March 8, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants