diff --git a/src/cli/args.test.ts b/src/cli/args.test.ts index 89c1119c..ddf49122 100644 --- a/src/cli/args.test.ts +++ b/src/cli/args.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, assert } from 'vitest'; -import { parseCli } from './args.js'; +import { parseCli, USAGE } from './args.js'; +import type { SharedRunOptions } from './args.js'; describe('parseCli', () => { it('defaults gc to cwd, 6h, no remote, not dry-run', () => { @@ -222,6 +223,113 @@ describe('parseCli', () => { expect(noFork.kind === 'error' && noFork.message).toMatch(/only applies with --fork/); }); + it('parses the shared RunOptions flags identically on run and watch (one parseRunOptions)', () => { + const shared = [ + '--provider', 'codex', '--review-provider', 'claude', '--fallback-provider', 'cursor', '--fallback-model', 'cursor-fast', + '--provider-model', 'gpt-5', '--review-model', 'claude-opus', '--escalate-model', 'claude-fable-5', + '--no-simplify', '--verify', 'pnpm test', '--visual-proof', 'pnpm shot', '--conformance', '--conformance-model', 'opus', + '--commit-author', 'Seba ', '--flow', 'flow-b', '--base', 'dev', '--max-turns', '80', '--max-repair-iterations', '5', + ]; + // Required<>: a RunOptions field added without an entry here fails to compile, as it does in parseRunOptions. + // --plan is asserted separately because it excludes --flow. + const expected: Required> = { + provider: 'codex', + reviewProvider: 'claude', + fallbackProvider: 'cursor', + fallbackModel: 'cursor-fast', + providerModel: 'gpt-5', + reviewModel: 'claude-opus', + escalateModel: 'claude-fable-5', + noSimplify: true, + verifyCmd: 'pnpm test', + visualProofCmd: 'pnpm shot', + conformance: true, + conformanceModel: 'opus', + commitAuthor: { name: 'Seba', email: 'seba@example.com' }, + flow: 'flow-b', + baseBranch: 'dev', + maxTurns: 80, + maxRepairIterations: 5, + }; + const run = parseCli(['run', '--github', 'o/r#1', ...shared], '/work'); + const watch = parseCli(['watch', '--source', 'github', '--label', 'vanguard', ...shared], '/work'); + expect(run).toMatchObject({ kind: 'run', ...expected }); + expect(watch).toMatchObject({ kind: 'watch', ...expected }); + for (const key of Object.keys(expected)) { + expect(watch[key as keyof typeof watch]).toEqual(run[key as keyof typeof run]); + } + expect(parseCli(['run', '--github', 'o/r#1', '--plan'], '/work')).toMatchObject({ kind: 'run', plan: true }); + expect(parseCli(['watch', '--source', 'github', '--label', 'vanguard', '--plan'], '/work')).toMatchObject({ kind: 'watch', plan: true }); + // Absent flags leave no key on either command. + const bareRun = parseCli(['run', '--github', 'o/r#1'], '/work'); + const bareWatch = parseCli(['watch', '--source', 'github', '--label', 'vanguard'], '/work'); + expect(bareRun.kind).toBe('run'); + expect(bareWatch.kind).toBe('watch'); + for (const key of [...Object.keys(expected), 'plan']) { + expect(key in bareRun).toBe(false); + expect(key in bareWatch).toBe(false); + } + }); + + it('carries every shared flag onto doctor except the stage-only three', () => { + const argv = [ + 'doctor', '--source', 'github', '--label', 'vanguard', + '--provider', 'codex', '--review-provider', 'claude', '--fallback-provider', 'cursor', '--fallback-model', 'cursor-fast', + '--provider-model', 'gpt-5', '--review-model', 'claude-opus', '--escalate-model', 'claude-fable-5', + '--no-simplify', '--verify', 'pnpm test', '--visual-proof', 'pnpm shot', '--conformance', '--conformance-model', 'opus', + '--commit-author', 'Seba ', '--flow', 'flow-b', '--base', 'dev', '--max-turns', '80', '--max-repair-iterations', '5', + ]; + const doctor = parseCli(argv, '/work'); + expect(doctor).toMatchObject({ + kind: 'doctor', + provider: 'codex', + reviewProvider: 'claude', + fallbackProvider: 'cursor', + fallbackModel: 'cursor-fast', + providerModel: 'gpt-5', + reviewModel: 'claude-opus', + escalateModel: 'claude-fable-5', + noSimplify: true, + verifyCmd: 'pnpm test', + commitAuthor: { name: 'Seba', email: 'seba@example.com' }, + flow: 'flow-b', + baseBranch: 'dev', + maxTurns: 80, + maxRepairIterations: 5, + }); + for (const key of ['visualProofCmd', 'conformance', 'conformanceModel']) expect(key in doctor).toBe(false); + }); + + it('rejects the run-only flags on watch and doctor instead of ignoring them', () => { + for (const command of ['watch', 'doctor']) { + const base = [command, '--source', 'linear', '--label', 'vanguard']; + expect(parseCli([...base, '--spec-file', 's.md'], '/work')).toMatchObject({ kind: 'error', message: expect.stringContaining('--spec-file applies to a single-issue run only') }); + expect(parseCli([...base, '--fork', '3'], '/work')).toMatchObject({ kind: 'error', message: expect.stringContaining(`--fork applies to run only; ${command} does not fork`) }); + expect(parseCli([...base, '--fork-scorer', 'llm'], '/work')).toMatchObject({ kind: 'error', message: expect.stringContaining(`${command} does not fork`) }); + } + // The same flags are fine on run. + expect(parseCli(['run', '--github', 'o/r#1', '--spec-file', 's.md', '--fork', '3', '--fork-scorer', 'llm'], '/work')).toMatchObject({ kind: 'run', specFile: 's.md', forkN: 3, forkScorer: 'llm' }); + }); + + it('documents the shared flags under both run and watch, and the run-only ones under run only', () => { + const watchStart = USAGE.indexOf('watch options (trigger'); + const runStart = USAGE.indexOf('run options (exactly one source)'); + const runEnd = USAGE.indexOf('review-pr options:'); + expect(watchStart).toBeGreaterThan(-1); + expect(runStart).toBeGreaterThan(watchStart); + expect(runEnd).toBeGreaterThan(runStart); + const watchBlock = USAGE.slice(watchStart, runStart); + const runBlock = USAGE.slice(runStart, runEnd); + for (const flag of ['--fallback-provider ', '--conformance-model ', '--max-repair-iterations ', '--base ', '--max-turns ']) { + expect(runBlock).toContain(flag); + expect(watchBlock).toContain(flag); + } + for (const flag of ['--fork ', '--fork-scorer ', '--spec-file ']) { + expect(runBlock).toContain(flag); + expect(watchBlock).not.toContain(flag); + } + }); + it('parses --provider-model and --review-model on run', () => { const cmd = parseCli(['run', '--linear', 'TES-1', '--provider-model', 'opus', '--review-model', 'haiku'], '/work'); expect(cmd.kind === 'run' && cmd.providerModel).toBe('opus'); diff --git a/src/cli/args.ts b/src/cli/args.ts index 5ee836f5..c7cd80d3 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -103,7 +103,7 @@ export type Command = provider?: ProviderName; llmProxy?: boolean; } - | { + | ({ kind: 'doctor'; /** Repair what the checks can repair (today: refresh the sandbox image's claude CLI). */ fix?: boolean; @@ -120,17 +120,8 @@ export type Command = repoSlug?: string; repoPath: string; skillsDir?: string; - provider?: string; - reviewProvider?: string; - fallbackProvider?: string; - fallbackModel?: string; - providerModel?: string; - reviewModel?: string; - /** Model for gate repairs after the first one failed (reactive escalation); default: stay on the implementer model. */ - escalateModel?: string; - /** How --fork variants are scored: an LLM verdict (default) or a decision model (clef via Cloudflare or VANGUARD_DECISION_URL). */ - forkScorer?: 'llm' | 'decision'; - verifyCmd?: string; + /** Cap on ready tasks per poll; parsed with watch's flags, read by nothing doctor runs. */ + maxTasks?: number; specModel?: string; specLabel?: string; agentLabel?: string; @@ -143,7 +134,7 @@ export type Command = specClaimedState?: string; specOnly?: boolean; llmProxy?: boolean; - } + } & Omit) | ({ kind: 'run'; source: 'linear' | 'github' | 'project' | 'gitlab'; @@ -223,7 +214,8 @@ export type Command = specClaimedState?: string; /** (loop-v1) Run only the spec pass each tick; the agent pass is skipped (no list, claim or run). */ specOnly?: boolean; - } & RunOptions) + // customProviders stays: dispatch loads the repo customs onto the command (watch.ts); only RunOnlyKey is excluded. + } & Omit) | { kind: 'review-mr'; iid: number; @@ -294,6 +286,64 @@ function parseForkScorer(raw: unknown): 'llm' | 'decision' | undefined | null { return raw === 'llm' || raw === 'decision' ? raw : null; } +/** + * RunOptions keys only `run` sets: one spec file describes one task, and fork variants need one + * implementer stage. watch processes every ready ticket per poll and never forks, so it rejects them. + */ +type RunOnlyKey = 'forkScorer' | 'specFile'; + +/** + * The RunOptions run and watch read off the same flags. `customProviders` is loaded from the target + * repo at dispatch (provider-choice.ts), never parsed. + */ +export type SharedRunOptions = Omit; + +/** Shared keys only a running stage reads: doctor's preflight never runs one, so they stay off its command. */ +type StageOnlyKey = 'visualProofCmd' | 'conformance' | 'conformanceModel'; + +/** + * Every key of T, present or not — so a RunOptions field added without a line in parseRunOptions + * is a type error, not a flag one subcommand silently drops. + */ +export type Exhaustive = { readonly [K in keyof T]-?: T[K] | undefined }; + +/** Drop the undefined entries: an absent flag leaves no key on the command (callers test with `in`). */ +function present(exhaustive: Exhaustive): T { + return Object.fromEntries(Object.entries(exhaustive).filter(([, value]) => value !== undefined)) as T; +} + +/** + * Shared flags other commands validate too (provider gates, --commit-author, --flow/--plan, the + * turn caps): parseCli checks them once, before any subcommand branch, and hands the results in. + */ +type CheckedRunFlags = Exhaustive< + Pick +>; + +/** The one CLI → RunOptions mapping, used by run and watch so the two cannot drift. */ +function parseRunOptions(values: Record, checked: CheckedRunFlags): SharedRunOptions { + return present({ + provider: checked.provider, + reviewProvider: checked.reviewProvider, + fallbackProvider: checked.fallbackProvider, + fallbackModel: checked.fallbackModel, + providerModel: typeof values['provider-model'] === 'string' ? values['provider-model'] : undefined, + reviewModel: typeof values['review-model'] === 'string' ? values['review-model'] : undefined, + escalateModel: typeof values['escalate-model'] === 'string' ? values['escalate-model'] : undefined, + noSimplify: values['no-simplify'] === true ? true : undefined, + verifyCmd: typeof values.verify === 'string' ? values.verify : undefined, + visualProofCmd: typeof values['visual-proof'] === 'string' ? values['visual-proof'] : undefined, + conformance: values.conformance === true ? true : undefined, + conformanceModel: typeof values['conformance-model'] === 'string' ? values['conformance-model'] : undefined, + commitAuthor: checked.commitAuthor, + plan: values.plan === true ? true : undefined, + flow: checked.flow, + baseBranch: typeof values.base === 'string' ? values.base : undefined, + maxTurns: checked.maxTurns, + maxRepairIterations: checked.maxRepairIterations, + }); +} + const HOUR_MS = 60 * 60 * 1000; const DEFAULT_MAX_AGE_HOURS = 6; const DEFAULT_CONCURRENCY = 2; @@ -798,6 +848,18 @@ export function parseCli(argv: string[], cwd: string): Command { }; } + // Shared by the two branches below; every command above returned already. + const runOptions = parseRunOptions(values, { + provider, + reviewProvider, + fallbackProvider, + fallbackModel, + commitAuthor, + flow: flowRaw, + maxTurns, + maxRepairIterations, + }); + if (positionals[0] === 'run') { const sources: Array<['linear' | 'github' | 'project' | 'gitlab', string]> = []; if (typeof values.linear === 'string') sources.push(['linear', values.linear]); @@ -830,6 +892,11 @@ export function parseCli(argv: string[], cwd: string): Command { // forkAndSelect runs every variant on the primary agent and returns before the stage fallback is // consulted, so the flag would be accepted and silently never fire. if (fallbackProvider !== undefined && Number.isFinite(forkN) && forkN >= 2) return fail('--fallback-provider does not apply to --fork variants; drop one of them.'); + // The single-task RunOptions (see RunOnlyKey): typed exhaustively so a new run-only field lands here. + const runOnly = present>({ + forkScorer, + specFile: typeof values['spec-file'] === 'string' ? values['spec-file'] : undefined, + }); return { kind: 'run', source: picked[0], @@ -846,26 +913,8 @@ export function parseCli(argv: string[], cwd: string): Command { ...(typeof values['github-repo'] === 'string' ? { repoSlug: values['github-repo'] } : {}), ...(typeof values.label === 'string' ? { label: values.label } : {}), ...(typeof values['gitlab-project'] === 'string' && picked[0] === 'gitlab' ? { project: values['gitlab-project'] } : {}), - ...(provider !== undefined ? { provider } : {}), - ...(reviewProvider !== undefined ? { reviewProvider } : {}), - ...(fallbackProvider !== undefined ? { fallbackProvider } : {}), - ...(fallbackModel !== undefined ? { fallbackModel } : {}), - ...(typeof values['provider-model'] === 'string' ? { providerModel: values['provider-model'] } : {}), - ...(typeof values['review-model'] === 'string' ? { reviewModel: values['review-model'] } : {}), - ...(typeof values['escalate-model'] === 'string' ? { escalateModel: values['escalate-model'] } : {}), - ...(forkScorer !== undefined ? { forkScorer } : {}), - ...(values['no-simplify'] === true ? { noSimplify: true } : {}), - ...(typeof values.verify === 'string' ? { verifyCmd: values.verify } : {}), - ...(typeof values['visual-proof'] === 'string' ? { visualProofCmd: values['visual-proof'] } : {}), - ...(values.conformance === true ? { conformance: true } : {}), - ...(typeof values['conformance-model'] === 'string' ? { conformanceModel: values['conformance-model'] } : {}), - ...(commitAuthor !== undefined ? { commitAuthor } : {}), - ...(values.plan === true ? { plan: true } : {}), - ...(flowRaw !== undefined ? { flow: flowRaw } : {}), - ...(typeof values.base === 'string' ? { baseBranch: values.base } : {}), - ...(maxTurns !== undefined ? { maxTurns } : {}), - ...(maxRepairIterations !== undefined ? { maxRepairIterations } : {}), - ...(typeof values['spec-file'] === 'string' ? { specFile: values['spec-file'] } : {}), + ...runOptions, + ...runOnly, }; } @@ -981,8 +1030,10 @@ export function parseCli(argv: string[], cwd: string): Command { if (values['max-tasks'] !== undefined && maxTasks === undefined) { return fail(`--max-tasks needs a positive integer, got "${String(values['max-tasks'])}".`); } - // watch has no --fork, so a fork scorer there could only be a silent no-op or a spurious failure. - if (values['fork-scorer'] !== undefined) return fail('--fork-scorer applies to `run --fork ` only; watch does not fork.'); + // The single-task flags (RunOnlyKey + --fork): on watch each would be a silent no-op or a spurious failure. + if (values['fork-scorer'] !== undefined) return fail(`--fork-scorer applies to \`run --fork \` only; ${commandKind} does not fork.`); + if (values.fork !== undefined) return fail(`--fork applies to run only; ${commandKind} does not fork.`); + if (values['spec-file'] !== undefined) return fail(`--spec-file applies to a single-issue run only; ${commandKind} has no single issue to attach it to.`); type WatchCommon = Omit, 'kind' | 'concurrency' | 'intervalMs' | 'once' | 'egress'>; const common: WatchCommon = { source, @@ -998,21 +1049,7 @@ export function parseCli(argv: string[], cwd: string): Command { ...(typeof values['review-state'] === 'string' ? { reviewState: values['review-state'] } : {}), ...(typeof values.skills === 'string' ? { skillsDir: values.skills } : {}), ...(typeof values['github-repo'] === 'string' ? { repoSlug: values['github-repo'] } : {}), - ...(provider !== undefined ? { provider } : {}), - ...(reviewProvider !== undefined ? { reviewProvider } : {}), - ...(fallbackProvider !== undefined ? { fallbackProvider } : {}), - ...(fallbackModel !== undefined ? { fallbackModel } : {}), - ...(typeof values['provider-model'] === 'string' ? { providerModel: values['provider-model'] } : {}), - ...(typeof values['review-model'] === 'string' ? { reviewModel: values['review-model'] } : {}), - ...(typeof values['escalate-model'] === 'string' ? { escalateModel: values['escalate-model'] } : {}), - ...(values['no-simplify'] === true ? { noSimplify: true } : {}), - ...(typeof values.verify === 'string' ? { verifyCmd: values.verify } : {}), - ...(commitAuthor !== undefined ? { commitAuthor } : {}), - ...(values.plan === true ? { plan: true } : {}), - ...(flowRaw !== undefined ? { flow: flowRaw } : {}), - ...(typeof values.base === 'string' ? { baseBranch: values.base } : {}), - ...(maxTurns !== undefined ? { maxTurns } : {}), - ...(maxRepairIterations !== undefined ? { maxRepairIterations } : {}), + ...runOptions, ...(proxyMode ? { llmProxy: true } : {}), // Loop v1 fields (omitted when not supplied, preserving existing behaviour when absent). ...(typeof values['spec-model'] === 'string' ? { specModel: values['spec-model'] } : {}), @@ -1029,15 +1066,14 @@ export function parseCli(argv: string[], cwd: string): Command { }; if (commandKind === 'doctor') { - return { kind: 'doctor', ...common, ...(values.fix === true ? { fix: true } : {}) }; + // StageOnlyKey: the proof/conformance knobs stay off the doctor command. + const { visualProofCmd: _visualProofCmd, conformance: _conformance, conformanceModel: _conformanceModel, ...checks } = common; + return { kind: 'doctor', ...checks, ...(values.fix === true ? { fix: true } : {}) }; } return { kind: 'watch', ...common, - ...(typeof values['visual-proof'] === 'string' ? { visualProofCmd: values['visual-proof'] } : {}), - ...(values.conformance === true ? { conformance: true } : {}), - ...(typeof values['conformance-model'] === 'string' ? { conformanceModel: values['conformance-model'] } : {}), concurrency: Number.isFinite(concurrency) && concurrency >= 1 ? Math.floor(concurrency) : DEFAULT_CONCURRENCY, intervalMs: (Number.isFinite(interval) && interval > 0 ? interval : 60) * 1000, once: values.once === true, @@ -1048,6 +1084,29 @@ export function parseCli(argv: string[], cwd: string): Command { return { kind: 'help' }; } +/** Option lines run and watch print verbatim — one source, so the two usage blocks cannot drift either. */ +const SHARED_RUN_OPTIONS_USAGE = ` --provider Provider that runs every stage (default: claude) + run/watch/doctor also accept a custom provider name from the repo's + .vanguard/app.json customProviders (S6) — direct mode only. + --review-provider Run only the review stage on this provider (cross-provider review) + --fallback-provider Retry on this provider when the primary one fails (outage, limit, revoked credential) — the implementer and every other stage on its provider; must sit on a different transport (e.g. codex for a claude implementer) + --fallback-model Model for every stage that fell back (default: that provider's default) + --provider-model Model for the implementer/simplifier stages (default: provider's default; zai -> glm-5.2) + --review-model Model for the review stage (default: provider's default) + --escalate-model Model for the 2nd and later gate repairs, once a repair on the implementer + model has failed (default: stay on the implementer model) + --no-simplify Skip the simplifier stage (lean: implement -> review only) + --verify Verification command for Proof of Work (overrides VANGUARD_VERIFY_CMD and auto-detect) + --visual-proof Visual proof command for UI artifacts (overrides VANGUARD_VISUAL_PROOF_CMD) + --conformance Run the conformance pass (planner-tier model checks diff against spec; opt-in) + --conformance-model Model for the conformance stage (default: same as implementer; 'opus' for planner-tier) + --commit-author Git author for the commit, "Name " (also enables white-label mode: feat/ branch, no Vanguard branding/review comment) + --plan Add a dedicated planning stage first (opus, high effort) before implement/review + --flow Run a named workflow (e.g. flow-b: plan -> implement -> adversary -> repair). --plan == --flow plan`; + +/** Printed after each section's own --base/--max-turns lines. */ +const MAX_REPAIR_ITERATIONS_USAGE = ' --max-repair-iterations Override the conformance/verify repair loop-back cap (default: 2)'; + export const USAGE = `vanguard Commands: @@ -1094,29 +1153,10 @@ Commands: state name "Spec", needs-info state "Needs Info"). For GitHub, a repo-only watch without --label also uses the routing-label defaults. --skills --repo --concurrency --egress (as for run) - --provider Provider that runs every stage (default: claude) - run/watch/doctor also accept a custom provider name from the repo's - .vanguard/app.json customProviders (S6) — direct mode only. - --review-provider Run only the review stage on this provider (cross-provider review) - --fallback-provider Retry on this provider when the primary one fails (outage, limit, revoked credential) — the implementer and every other stage on its provider; must sit on a different transport (e.g. codex for a claude implementer) - --fallback-model Model for every stage that fell back (default: that provider's default) - --provider-model Model for the implementer/simplifier stages (default: provider's default) - --review-model Model for the review stage (default: provider's default) - --escalate-model Model for the 2nd and later gate repairs, once a repair on the implementer - model has failed (default: stay on the implementer model) - --fork-scorer How --fork variants are scored: a one-shot LLM verdict (default) or a - decision model (clef; needs CLOUDFLARE_ACCOUNT_ID+CLOUDFLARE_AUTH_TOKEN or VANGUARD_DECISION_URL) - --no-simplify Skip the simplifier stage (lean: implement -> review only) - --verify Verification command for Proof of Work (overrides VANGUARD_VERIFY_CMD and auto-detect) - --visual-proof Visual proof command for UI artifacts (overrides VANGUARD_VISUAL_PROOF_CMD) - --conformance Run the conformance pass (planner-tier model checks diff against spec; opt-in) - --conformance-model Model for the conformance stage (default: same as implementer; 'opus' for planner-tier) - --commit-author Git author for the commit, "Name " (also enables white-label mode: feat/ branch, no Vanguard branding/review comment) +${SHARED_RUN_OPTIONS_USAGE} --base Base branch to branch off and target the PR at; also the loop-v1 spec pass's research baseline (default: main) - --plan Add a dedicated planning stage first (opus, high effort) before implement/review - --flow Run a named workflow (e.g. flow-b: plan -> implement -> adversary -> repair). --plan == --flow plan --max-turns Override the implementer (or loop-v1 spec pass's tech-spec) stage turn cap (default: 30; opt-in, higher cost) - --max-repair-iterations Override the conformance/verify repair loop-back cap (default: 2) +${MAX_REPAIR_ITERATIONS_USAGE} Note (project): Status option names must match the project's Status field exactly. Resolve field and option IDs with: gh project field-list --owner --format json @@ -1190,29 +1230,14 @@ Commands: --skills Skills directory to inject (Linear: the linear-cli skill) --github-repo GitHub repo slug (default: detected from origin) --concurrency (parent/project) max tasks at once (default: 2) - --provider Provider that runs every stage (default: claude) - run/watch/doctor also accept a custom provider name from the repo's - .vanguard/app.json customProviders (S6) — direct mode only. - --review-provider Run only the review stage on this provider (cross-provider review) - --fallback-provider Retry on this provider when the primary one fails (outage, limit, revoked credential) — the implementer and every other stage on its provider; must sit on a different transport (e.g. codex for a claude implementer) - --fallback-model Model for every stage that fell back (default: that provider's default) - --provider-model Model for the implementer/simplifier stages (default: provider's default; zai -> glm-5.2) - --review-model Model for the review stage (default: provider's default) - --escalate-model Model for the 2nd and later gate repairs, once a repair on the implementer - model has failed (default: stay on the implementer model) - --no-simplify Skip the simplifier stage (lean: implement -> review only) - --fork Run the implementer as n variants (n>=2) and keep the best-scored diff - --verify Verification command for Proof of Work (overrides VANGUARD_VERIFY_CMD and auto-detect) - --visual-proof Visual proof command for UI artifacts (overrides VANGUARD_VISUAL_PROOF_CMD) - --conformance Run the conformance pass (planner-tier model checks diff against spec; opt-in) - --conformance-model Model for the conformance stage (default: same as implementer; 'opus' for planner-tier) - --commit-author Git author for the commit, "Name " (also enables white-label mode: feat/ branch, no Vanguard branding/review comment) +${SHARED_RUN_OPTIONS_USAGE} --base Base branch to branch off and target the PR at (default: main) - --plan Add a dedicated planning stage first (opus, high effort) before implement/review - --flow Run a named workflow (e.g. flow-b: plan -> implement -> adversary -> repair). --plan == --flow plan --max-turns Override the implementer stage turn cap (default: 30; opt-in, higher cost) - --max-repair-iterations Override the conformance/verify repair loop-back cap (default: 2) - --spec-file Inject a local spec file as a virtual issue comment (implementer + conformance read it; nothing is posted to the tracker) +${MAX_REPAIR_ITERATIONS_USAGE} + --fork Run the implementer as n variants (n>=2) and keep the best-scored diff + --fork-scorer How --fork variants are scored: a one-shot LLM verdict (default) or a + decision model (clef; needs CLOUDFLARE_ACCOUNT_ID+CLOUDFLARE_AUTH_TOKEN or VANGUARD_DECISION_URL) + --spec-file Inject a local spec file as a virtual issue comment (implementer + conformance read it; nothing is posted to the tracker) review-pr options: GitHub PR URL, owner/repo#number, or bare number with --github-repo @@ -1366,6 +1391,7 @@ Commands: doctor options: Uses the same source/routing flags as watch, but only runs AFK preflight checks and exits. + Like watch, it rejects the run-only flags --fork, --fork-scorer and --spec-file. Add --spec-only to check a spec-only loop-v1 watch: it skips the checks that only guard publishing. Example (GitHub): vanguard doctor --source github --github-repo owner/repo Example (Linear): vanguard doctor --loop-v1 --label vanguard --skills ./skills diff --git a/src/cli/run-options.fixture.ts b/src/cli/run-options.fixture.ts index 473e3f39..eb9ad750 100644 --- a/src/cli/run-options.fixture.ts +++ b/src/cli/run-options.fixture.ts @@ -1,11 +1,16 @@ +import type { Exhaustive, SharedRunOptions } from './args.js'; + /** * Every shared RunOptions flag set to a non-default value. The run/watch deps-threading tests assert * each per-source builder carries this whole object through `pickRunOptions`, so a dropped spread is - * caught — a guard the type system cannot give, since RunOptions' fields are all optional. + * caught — a guard the type system cannot give, since RunOptions' fields are all optional. The + * `satisfies` keeps this list itself complete: a new shared field without a value here fails to compile. */ export const RUN_OPTIONS = { provider: 'codex', reviewProvider: 'cursor', + fallbackProvider: 'claude', + fallbackModel: 'claude-sonnet-5-5', providerModel: 'gpt-5', reviewModel: 'claude-opus', noSimplify: true, @@ -22,4 +27,4 @@ export const RUN_OPTIONS = { maxTurns: 80, maxRepairIterations: 5, escalateModel: 'claude-fable-5', -} as const; +} as const satisfies Exhaustive;