From 7d915568cda564a00bb240ff2319dca3666da7b4 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 3 Apr 2026 17:50:24 +0300 Subject: [PATCH 1/4] feat(watch): add --verbose flag for debugging empty boards and silent failures (#781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New --verbose / -v flag outputs diagnostic info: - Startup: repo, platform, auth account, agent-cmd, dispatch mode - Per-round: issues found with labels/assignees, PRs with review status - Execute: agent command, batch selection, dispatch classification - Timing: round duration - Warnings: unused positional args, auth failures Also warns when positional args are passed to watch (they're ignored). New file: verbose.ts — createVerboseLogger utility (log/warn/section/table) Closes #781 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/watch-verbose-mode.md | 5 ++ packages/squad-cli/src/cli-entry.ts | 10 +++ .../commands/watch/capabilities/execute.ts | 11 +++ .../src/cli/commands/watch/config.ts | 6 +- .../squad-cli/src/cli/commands/watch/index.ts | 68 ++++++++++++++++++- .../squad-cli/src/cli/commands/watch/types.ts | 2 + .../src/cli/commands/watch/verbose.ts | 26 +++++++ 7 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 .changeset/watch-verbose-mode.md create mode 100644 packages/squad-cli/src/cli/commands/watch/verbose.ts diff --git a/.changeset/watch-verbose-mode.md b/.changeset/watch-verbose-mode.md new file mode 100644 index 000000000..3dc92ae8d --- /dev/null +++ b/.changeset/watch-verbose-mode.md @@ -0,0 +1,5 @@ +--- +"@bradygaster/squad-cli": minor +--- + +Add --verbose flag to squad watch for debugging empty boards and silent failures (#781) diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index 3c7e970c4..e3bdb70b7 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -349,6 +349,8 @@ async function main(): Promise { const execute = args.includes('--execute') ? true : undefined; + const verbose = args.includes('--verbose') || args.includes('-v'); + const copilotFlagsIdx = args.indexOf('--copilot-flags'); const copilotFlags = (copilotFlagsIdx !== -1 && args[copilotFlagsIdx + 1]) ? args[copilotFlagsIdx + 1] @@ -394,9 +396,17 @@ async function main(): Promise { timeout, copilotFlags, agentCmd, + verbose, capabilities: Object.keys(capabilities).length > 0 ? capabilities : undefined, }); + // After parsing all flags, check for positional args that look like prompts + const watchArgStart = args.indexOf(cmd) + 1; + const positionalArgs = args.slice(watchArgStart).filter(a => !a.startsWith('--') && !a.startsWith('-')); + if (positionalArgs.length > 0 && config.verbose) { + console.log(`[verbose] ⚠️ Positional args ignored by watch: "${positionalArgs.join(' ')}". Use --execute to process issues.`); + } + await runWatch(process.cwd(), config); return; } diff --git a/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts b/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts index a0dcb5f8e..272477a90 100644 --- a/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts +++ b/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts @@ -5,6 +5,7 @@ import { execFile, type ChildProcess } from 'node:child_process'; import type { WatchCapability, WatchContext, PreflightResult, CapabilityResult } from '../types.js'; import type { MachineCapabilities } from '@bradygaster/squad-sdk/ralph/capabilities'; +import { createVerboseLogger } from '../verbose.js'; /** Normalized work item for execution. */ export interface ExecutableWorkItem { @@ -114,10 +115,14 @@ export class ExecuteCapability implements WatchCapability { } async execute(context: WatchContext): Promise { + const vlog = createVerboseLogger(context.verbose ?? false); + try { const maxConcurrent = (context.config['maxConcurrent'] as number) ?? 1; const timeout = ((context.config['timeout'] as number) ?? 30) * 60_000; + vlog.log(`Execute: agentCmd=${context.agentCmd ?? 'gh copilot'}, maxConcurrent=${maxConcurrent}, timeout=${timeout / 60_000}m`); + // Fetch open issues with squad label const sdkItems = await context.adapter.listWorkItems({ tags: ['squad'], state: 'open', limit: 50 }); const issues: ExecutableWorkItem[] = sdkItems.map(wi => ({ @@ -135,6 +140,12 @@ export class ExecuteCapability implements WatchCapability { const executable = findExecutableIssues(context.roster, capabilities, handled); const batch = executable.slice(0, maxConcurrent); + vlog.log(`Execute: ${issues.length} total issues, ${handled.length} capable, ${executable.length} executable, ${batch.length} in batch`); + for (const issue of batch) { + const labels = issue.labels.map(l => l.name).join(', '); + vlog.log(` → #${issue.number}: "${issue.title}" [${labels}]`); + } + if (batch.length === 0) { return { success: true, summary: 'no executable issues' }; } diff --git a/packages/squad-cli/src/cli/commands/watch/config.ts b/packages/squad-cli/src/cli/commands/watch/config.ts index ac7509d72..985aca8c9 100644 --- a/packages/squad-cli/src/cli/commands/watch/config.ts +++ b/packages/squad-cli/src/cli/commands/watch/config.ts @@ -20,6 +20,8 @@ export interface WatchConfig { agentCmd?: string; /** Per-capability config: `true` / `false` / object with sub-options. */ capabilities: Record>; + /** Enable verbose diagnostic output for debugging. */ + verbose?: boolean; } const DEFAULTS: WatchConfig = { @@ -68,6 +70,7 @@ export function loadWatchConfig( ...(fileConfig.capabilities ?? {}), ...(cliOverrides.capabilities ?? {}), }, + verbose: cliOverrides.verbose ?? fileConfig.verbose ?? false, }; return merged; @@ -83,10 +86,11 @@ function normalizeFileConfig(raw: Record): Partial if (typeof raw['timeout'] === 'number') result.timeout = raw['timeout']; if (typeof raw['copilotFlags'] === 'string') result.copilotFlags = raw['copilotFlags']; if (typeof raw['agentCmd'] === 'string') result.agentCmd = raw['agentCmd']; + if (typeof raw['verbose'] === 'boolean') result.verbose = raw['verbose']; // Everything else is a capability key const caps: Record> = {}; - const reserved = new Set(['interval', 'execute', 'maxConcurrent', 'timeout', 'copilotFlags', 'agentCmd']); + const reserved = new Set(['interval', 'execute', 'maxConcurrent', 'timeout', 'copilotFlags', 'agentCmd', 'verbose']); for (const [key, value] of Object.entries(raw)) { if (reserved.has(key)) continue; if (typeof value === 'boolean' || (typeof value === 'object' && value !== null && !Array.isArray(value))) { diff --git a/packages/squad-cli/src/cli/commands/watch/index.ts b/packages/squad-cli/src/cli/commands/watch/index.ts index 5f1bad662..55d7a74b3 100644 --- a/packages/squad-cli/src/cli/commands/watch/index.ts +++ b/packages/squad-cli/src/cli/commands/watch/index.ts @@ -39,6 +39,7 @@ import type { WatchConfig } from './config.js'; import type { WatchCapability, WatchContext, WatchPhase, CapabilityResult } from './types.js'; import { CapabilityRegistry } from './registry.js'; import { createDefaultRegistry } from './capabilities/index.js'; +import { createVerboseLogger, type VerboseLogger } from './verbose.js'; // ── Re-exports for backward compatibility ──────────────────────── @@ -47,6 +48,7 @@ export { loadWatchConfig } from './config.js'; export type { WatchCapability, WatchContext, WatchPhase, PreflightResult, CapabilityResult } from './types.js'; export { CapabilityRegistry } from './registry.js'; export { createDefaultRegistry } from './capabilities/index.js'; +export { createVerboseLogger, type VerboseLogger } from './verbose.js'; // ── Watch Platform Abstraction ─────────────────────────────────── @@ -193,13 +195,18 @@ type PRBoardState = Pick, adapter: PlatformAdapter): Promise { +async function checkPRs(roster: ReturnType, adapter: PlatformAdapter, vlog?: VerboseLogger): Promise { const timestamp = new Date().toLocaleTimeString(); const prs = await listWatchPullRequests(adapter, { state: 'open', limit: 20 }); const squadPRs = prs.filter(pr => pr.labels.some(l => l.name.startsWith('squad')) || pr.headRefName.startsWith('squad/'), ); + vlog?.log(`PRs found: ${prs.length} total`); + for (const pr of squadPRs) { + vlog?.log(` PR #${pr.number}: "${pr.title}" draft=${pr.isDraft} review=${pr.reviewDecision ?? 'none'}`); + } + if (squadPRs.length === 0) { return { drafts: 0, needsReview: 0, changesRequested: 0, ciFailures: 0, readyToMerge: 0, totalOpen: 0 }; } @@ -272,11 +279,19 @@ async function runCheck( autoAssign: boolean, capabilities: MachineCapabilities | null, adapter: PlatformAdapter, + vlog?: VerboseLogger, ): Promise { const timestamp = new Date().toLocaleTimeString(); try { const issues = await listWatchWorkItems(adapter, { label: 'squad', state: 'open', limit: 20 }); + vlog?.log(`Issues found: ${issues.length} total`); + for (const issue of issues) { + const labels = issue.labels?.map((l: { name: string }) => l.name).join(', ') ?? 'none'; + const assignees = issue.assignees?.map((a: { login: string }) => a.login).join(', ') ?? 'none'; + vlog?.log(` #${issue.number}: "${issue.title}" [${labels}] assignees=[${assignees}]`); + } + const { filterByCapabilities } = await import('@bradygaster/squad-sdk/ralph/capabilities'); const { handled: capableIssues, skipped: incapableIssues } = filterByCapabilities(issues, capabilities); @@ -329,7 +344,7 @@ async function runCheck( } } - const prState = await checkPRs(roster, adapter); + const prState = await checkPRs(roster, adapter, vlog); return { untriaged: untriaged.length, assigned: assignedIssues.length, executed: 0, ...prState }; } catch (e) { console.error(`${RED}✗${RESET} [${timestamp}] Check failed: ${(e as Error).message}`); @@ -635,6 +650,32 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig return fatal(`Could not detect platform: ${(err as Error).message}`); } + // Verbose logger + const vlog = createVerboseLogger(config.verbose ?? false); + + vlog.section('Startup'); + vlog.table({ + repo: teamRoot, + platform: adapter.type, + verbose: true, + interval: `${config.interval}m`, + execute: config.execute ?? false, + agentCmd: config.agentCmd ?? '(default: gh copilot)', + dispatchMode: config.capabilities['wave-dispatch'] ? 'wave' : 'task', + maxConcurrent: config.maxConcurrent ?? 1, + }); + + // Auth check (verbose) + if (adapter.type === 'github') { + try { + const authOut = execFileSync('gh', ['auth', 'status'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + const activeAccount = authOut.match(/Logged in to .* account (\S+)/)?.[1]; + vlog.log(`Auth: ${activeAccount ?? 'unknown'}`); + } catch { + vlog.warn('gh auth status failed — API calls may fail silently'); + } + } + // Verify platform CLI availability if (adapter.type === 'github') { if (!(await ghAvailable())) fatal('gh CLI not found — install from https://cli.github.com'); @@ -688,6 +729,7 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig config: {}, agentCmd: config.agentCmd, copilotFlags: config.copilotFlags, + verbose: config.verbose, }; const enabledCapabilities = await preflightCapabilities(registry, config, baseContext); @@ -713,6 +755,7 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig async function executeRound(): Promise { const ts = new Date().toLocaleTimeString(); + const roundStart = Date.now(); // Circuit breaker gate if (cbState.status === 'open') { @@ -756,6 +799,8 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig round++; const roundContext: WatchContext = { ...baseContext, round }; + vlog.section(`Round ${round}`); + // Phase 1: pre-scan (self-pull, subsquad discovery) await runPhase('pre-scan', enabledCapabilities, roundContext, config); @@ -766,7 +811,7 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig } // Core: triage (always runs — not a capability) - const roundState = await runCheck(rules, modules, roster, hasCopilot, autoAssign, capabilities, adapter); + const roundState = await runCheck(rules, modules, roster, hasCopilot, autoAssign, capabilities, adapter, vlog); // Phase 2: post-triage (two-pass hydration) await runPhase('post-triage', enabledCapabilities, roundContext, config); @@ -790,6 +835,19 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig timestamp: new Date(), }); await monitor.healthCheck(); + + // Verbose board counters + vlog.table({ + untriaged: roundState.untriaged, + assigned: roundState.assigned, + drafts: roundState.drafts, + needsReview: roundState.needsReview, + changesRequested: roundState.changesRequested, + ciFailures: roundState.ciFailures, + readyToMerge: roundState.readyToMerge, + executed: roundState.executed, + }); + reportBoard(roundState, round); // Post-round: update circuit breaker on success @@ -806,6 +864,10 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig cbState.consecutiveFailures = 0; } saveCBState(squadDirInfo.path, cbState); + + const roundEnd = Date.now(); + const elapsed = ((roundEnd - roundStart) / 1000).toFixed(1); + vlog.log(`Round ${round} complete (${elapsed}s)`); } // Run immediately, then on interval diff --git a/packages/squad-cli/src/cli/commands/watch/types.ts b/packages/squad-cli/src/cli/commands/watch/types.ts index f20b06206..b41e76bc2 100644 --- a/packages/squad-cli/src/cli/commands/watch/types.ts +++ b/packages/squad-cli/src/cli/commands/watch/types.ts @@ -37,6 +37,8 @@ export interface WatchContext { /** Hidden --agent-cmd override. */ agentCmd?: string; copilotFlags?: string; + /** Verbose diagnostic output enabled. */ + verbose?: boolean; } /** Contract that every watch capability must implement. */ diff --git a/packages/squad-cli/src/cli/commands/watch/verbose.ts b/packages/squad-cli/src/cli/commands/watch/verbose.ts new file mode 100644 index 000000000..02fce06ea --- /dev/null +++ b/packages/squad-cli/src/cli/commands/watch/verbose.ts @@ -0,0 +1,26 @@ +/** + * Verbose logger for squad watch debugging. + * Only outputs when --verbose is enabled. + */ +export function createVerboseLogger(enabled: boolean) { + return { + log: (...args: unknown[]) => { + if (enabled) console.log('[verbose]', ...args); + }, + warn: (...args: unknown[]) => { + if (enabled) console.log('[verbose] ⚠️', ...args); + }, + section: (title: string) => { + if (enabled) console.log(`[verbose] ── ${title} ──`); + }, + table: (data: Record) => { + if (enabled) { + for (const [k, v] of Object.entries(data)) { + console.log(`[verbose] ${k}: ${v}`); + } + } + }, + }; +} + +export type VerboseLogger = ReturnType; From 1f8e6015c5aae396295429ee035c6daa812b6a5a Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 3 Apr 2026 23:50:20 +0300 Subject: [PATCH 2/4] fix: address Copilot review comments on --verbose PR (#782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate gh auth status behind config.verbose (avoid unnecessary process spawn) - Fix startup table verbose: true → config.verbose - Fix positional args false-positive: skip values after known value-flags Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-cli/src/cli-entry.ts | 15 +++++++++++++-- .../squad-cli/src/cli/commands/watch/index.ts | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index e3bdb70b7..1267ec8f9 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -400,9 +400,20 @@ async function main(): Promise { capabilities: Object.keys(capabilities).length > 0 ? capabilities : undefined, }); - // After parsing all flags, check for positional args that look like prompts + // After parsing all flags, check for positional args that look like prompts. + // Skip values that follow known value-flags (e.g. "--interval 5" → "5" is not positional). + const knownValueFlags = new Set([ + '--interval', '--copilot-flags', '--agent-cmd', '--max-concurrent', '--timeout', '--board-project', + ]); const watchArgStart = args.indexOf(cmd) + 1; - const positionalArgs = args.slice(watchArgStart).filter(a => !a.startsWith('--') && !a.startsWith('-')); + const watchArgs = args.slice(watchArgStart); + const positionalArgs: string[] = []; + for (let i = 0; i < watchArgs.length; i++) { + const arg = watchArgs[i]!; + if (knownValueFlags.has(arg)) { i++; continue; } + if (arg.startsWith('-')) continue; + positionalArgs.push(arg); + } if (positionalArgs.length > 0 && config.verbose) { console.log(`[verbose] ⚠️ Positional args ignored by watch: "${positionalArgs.join(' ')}". Use --execute to process issues.`); } diff --git a/packages/squad-cli/src/cli/commands/watch/index.ts b/packages/squad-cli/src/cli/commands/watch/index.ts index 55d7a74b3..2bbe6db51 100644 --- a/packages/squad-cli/src/cli/commands/watch/index.ts +++ b/packages/squad-cli/src/cli/commands/watch/index.ts @@ -657,7 +657,7 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig vlog.table({ repo: teamRoot, platform: adapter.type, - verbose: true, + verbose: config.verbose ?? false, interval: `${config.interval}m`, execute: config.execute ?? false, agentCmd: config.agentCmd ?? '(default: gh copilot)', @@ -665,8 +665,8 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig maxConcurrent: config.maxConcurrent ?? 1, }); - // Auth check (verbose) - if (adapter.type === 'github') { + // Auth check (verbose only — avoid unnecessary process spawn on every startup) + if (config.verbose && adapter.type === 'github') { try { const authOut = execFileSync('gh', ['auth', 'status'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); const activeAccount = authOut.match(/Logged in to .* account (\S+)/)?.[1]; From e3566459d3bdaabab2af0d3ceff37cb4c6314df3 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 4 Apr 2026 18:55:46 +0300 Subject: [PATCH 3/4] fix: address Copilot review comments - Add clarifying comment explaining verbose:true is expected in startup table (table only renders when verbose mode is active) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-cli/src/cli/commands/watch/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/squad-cli/src/cli/commands/watch/index.ts b/packages/squad-cli/src/cli/commands/watch/index.ts index fcdc7ec34..a9a82c8f3 100644 --- a/packages/squad-cli/src/cli/commands/watch/index.ts +++ b/packages/squad-cli/src/cli/commands/watch/index.ts @@ -657,6 +657,7 @@ export async function runWatch(dest: string, options: WatchOptions | WatchConfig vlog.table({ repo: teamRoot, platform: adapter.type, + // This table only prints when verbose mode is active, so verbose is always true here. verbose: config.verbose ?? false, interval: `${config.interval}m`, execute: config.execute ?? false, From 820b1484a9bab1abc02fba15f9893a577c08889a Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Sat, 4 Apr 2026 23:46:04 +0300 Subject: [PATCH 4/4] fix: remove undefined maxConcurrent ref in execute.ts verbose log --- .../squad-cli/src/cli/commands/watch/capabilities/execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts b/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts index 8831e4af7..8954c6480 100644 --- a/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts +++ b/packages/squad-cli/src/cli/commands/watch/capabilities/execute.ts @@ -190,7 +190,7 @@ export class ExecuteCapability implements WatchCapability { try { const timeout = ((context.config['timeout'] as number) ?? 30) * 60_000; - vlog.log(`Execute: agentCmd=${context.agentCmd ?? 'gh copilot'}, maxConcurrent=${maxConcurrent}, timeout=${timeout / 60_000}m`); + vlog.log(`Execute: agentCmd=${context.agentCmd ?? 'gh copilot'}, timeout=${timeout / 60_000}m`); // Fetch open issues with squad label const sdkItems = await context.adapter.listWorkItems({ tags: ['squad'], state: 'open', limit: 50 });