diff --git a/.changeset/forge-two-pass-adapter.md b/.changeset/forge-two-pass-adapter.md new file mode 100644 index 000000000..11208ce8e --- /dev/null +++ b/.changeset/forge-two-pass-adapter.md @@ -0,0 +1,8 @@ +--- +"@bradygaster/squad-sdk": minor +"@bradygaster/squad-cli": patch +--- + +feat(platform): hydrate work-item `body` via `getWorkItem` and route two-pass scan through the adapter + +`WorkItem` now carries an optional `body`, populated by both `GitHubAdapter` (`gh issue view --json body`) and `AzureDevOpsAdapter` (`System.Description`). The watch `two-pass` capability hydrates actionable items through `adapter.getWorkItem` instead of a hardcoded `gh issue view`, so it works on Azure DevOps as well as GitHub. diff --git a/packages/squad-cli/src/cli/commands/watch/capabilities/two-pass.ts b/packages/squad-cli/src/cli/commands/watch/capabilities/two-pass.ts index 1941d42ee..8c64d977a 100644 --- a/packages/squad-cli/src/cli/commands/watch/capabilities/two-pass.ts +++ b/packages/squad-cli/src/cli/commands/watch/capabilities/two-pass.ts @@ -2,12 +2,8 @@ * TwoPass capability — lightweight list then hydrate actionable issues only. */ -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; import type { WatchCapability, WatchContext, PreflightResult, CapabilityResult } from '../types.js'; -const execFileAsync = promisify(execFile); - /** Labels that block autonomous execution. */ const BLOCKED_LABELS: ReadonlySet = new Set([ 'status:blocked', 'status:waiting-external', 'status:postponed', @@ -19,7 +15,7 @@ export class TwoPassCapability implements WatchCapability { readonly name = 'two-pass'; readonly description = 'Lightweight scan then hydrate only actionable issues'; readonly configShape = 'boolean' as const; - readonly requires = ['gh']; + readonly requires = []; readonly phase = 'post-triage' as const; async preflight(_context: WatchContext): Promise { @@ -45,15 +41,18 @@ export class TwoPassCapability implements WatchCapability { return true; }); - // Pass 2: hydrate actionable issues (fetch body + comments) + // Pass 2: hydrate actionable issues (fetch body) via the platform adapter. + // Some adapters (e.g. ADO) already populate body during listWorkItems, + // so skip the extra fetch when it's already present. const hydrated: Array<{ number: number; title: string; body?: string }> = []; for (const item of actionable) { + if (item.body !== undefined) { + hydrated.push({ number: item.id, title: item.title, body: item.body }); + continue; + } try { - const { stdout: detailJson } = await execFileAsync('gh', [ - 'issue', 'view', String(item.id), - '--json', 'number,title,body,labels,assignees', - ], { maxBuffer: 5 * 1024 * 1024 }); - hydrated.push(JSON.parse(detailJson)); + const full = await context.adapter.getWorkItem(item.id); + hydrated.push({ number: full.id, title: full.title, body: full.body }); } catch { hydrated.push({ number: item.id, title: item.title }); } diff --git a/packages/squad-sdk/src/platform/azure-devops.ts b/packages/squad-sdk/src/platform/azure-devops.ts index e9036be24..101647c59 100644 --- a/packages/squad-sdk/src/platform/azure-devops.ts +++ b/packages/squad-sdk/src/platform/azure-devops.ts @@ -228,6 +228,7 @@ export class AzureDevOpsAdapter implements PlatformAdapter { state: (fields['System.State'] as string) ?? '', tags, assignedTo: assignedTo?.displayName ?? assignedTo?.uniqueName, + body: typeof fields['System.Description'] === 'string' ? (fields['System.Description'] as string) : undefined, url: wi._links?.html?.href ?? wi.url, }; } diff --git a/packages/squad-sdk/src/platform/github.ts b/packages/squad-sdk/src/platform/github.ts index 91d16c2f9..c7831f179 100644 --- a/packages/squad-sdk/src/platform/github.ts +++ b/packages/squad-sdk/src/platform/github.ts @@ -67,7 +67,7 @@ export class GitHubAdapter implements PlatformAdapter { async getWorkItem(id: number): Promise { const output = this.gh([ 'issue', 'view', String(id), '--repo', this.repoFlag, - '--json', 'number,title,state,labels,assignees,url', + '--json', 'number,title,state,labels,assignees,url,body', ]); const issue = parseJson<{ number: number; @@ -76,6 +76,7 @@ export class GitHubAdapter implements PlatformAdapter { labels: Array<{ name: string }>; assignees: Array<{ login: string }>; url: string; + body?: string; }>(output); return { @@ -84,6 +85,7 @@ export class GitHubAdapter implements PlatformAdapter { state: issue.state.toLowerCase(), tags: issue.labels.map((l) => l.name), assignedTo: issue.assignees[0]?.login, + body: issue.body, url: issue.url, }; } diff --git a/packages/squad-sdk/src/platform/types.ts b/packages/squad-sdk/src/platform/types.ts index ae33ea0d2..199d454aa 100644 --- a/packages/squad-sdk/src/platform/types.ts +++ b/packages/squad-sdk/src/platform/types.ts @@ -23,6 +23,7 @@ export interface WorkItem { state: string; tags: string[]; assignedTo?: string; + body?: string; url: string; }