From f4f1221c69ed24674412536a8b6a4ec54a31aa40 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 20:14:00 +0000 Subject: [PATCH] refactor: remove the primary coding agent question and stored preference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setup no longer asks "Which coding agent do you mainly use?" nor silently persists a single detected agent; the `agent` config key, POLYLANE_AGENT, and every consumer are gone. `map` now resolves its agent from the --agent flag, the only installed agent, or a map-time prompt (any runnable installed agent when non-interactive); the `integration connect` picker keeps plain catalog order. Legacy `agent` fields in existing config files are silently ignored โ€” no migration. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_0188rapzBDdq1zvp7JYaxxpu --- src/agents/registry.ts | 15 ------- src/commands/config/set.ts | 7 +-- src/commands/config/show.ts | 1 - src/commands/integration/connect.ts | 25 +---------- src/commands/map.ts | 12 ++--- src/commands/setup.ts | 55 ++--------------------- src/config/loader.ts | 8 ---- src/config/schema.ts | 3 -- test/config.test.ts | 12 ----- test/integration-connect-priority.test.ts | 39 ---------------- test/loader.test.ts | 39 +++++++++++----- test/setup.test.ts | 37 --------------- 12 files changed, 37 insertions(+), 216 deletions(-) delete mode 100644 test/integration-connect-priority.test.ts diff --git a/src/agents/registry.ts b/src/agents/registry.ts index 462f0ec..4d6a772 100644 --- a/src/agents/registry.ts +++ b/src/agents/registry.ts @@ -4,8 +4,6 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { applyEdits, modify, parse as parseJsonc, type ParseError } from 'jsonc-parser'; -import { CLIError } from '../errors/base'; -import { ExitCode } from '../errors/codes'; import { ensureDir } from '../utils/fs'; import { SKILL_MD } from '../generated/skill'; @@ -491,16 +489,3 @@ export function agentById(id: string): AgentSetup | undefined { return AGENTS.find((a) => a.id === id); } -export function isAgentId(id: string): boolean { - return AGENTS.some((a) => a.id === id); -} - -export function validateAgentId(id: string): void { - if (!isAgentId(id)) { - throw new CLIError( - `Unknown agent: "${id}"`, - ExitCode.USAGE, - `Supported agents: ${AGENT_IDS.join(', ')}` - ); - } -} diff --git a/src/commands/config/set.ts b/src/commands/config/set.ts index ca2cdc7..6d996f6 100644 --- a/src/commands/config/set.ts +++ b/src/commands/config/set.ts @@ -8,12 +8,11 @@ import { validateWorkspaceId, } from '../../config/schema'; import { writeConfigFile } from '../../config/loader'; -import { validateAgentId } from '../../agents/registry'; import { CLIError } from '../../errors/base'; import { ExitCode } from '../../errors/codes'; import { requireArg } from '../helpers'; -const VALID_KEYS = new Set(['domain', 'workspace_id', 'api_key', 'agent', 'output', 'timeout', 'telemetry', 'hints']); +const VALID_KEYS = new Set(['domain', 'workspace_id', 'api_key', 'output', 'timeout', 'telemetry', 'hints']); function parseBooleanValue(key: string, value: string): boolean { const truthy = ['1', 'true', 'yes', 'on', 'enabled']; @@ -65,10 +64,6 @@ export const configSetCommand: Command = { validateApiKey(value); partial.api_key = value; break; - case 'agent': - validateAgentId(value); - partial.agent = value; - break; case 'output': { validateOutput(value); partial.output = value; diff --git a/src/commands/config/show.ts b/src/commands/config/show.ts index 12553a0..a6a3773 100644 --- a/src/commands/config/show.ts +++ b/src/commands/config/show.ts @@ -17,7 +17,6 @@ export const configShowCommand: Command = { const result = { domain: config.domain, workspaceId: config.workspaceId ?? null, - agent: config.agent ?? null, output: config.output, timeout: config.timeout, hints: config.hints, diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index 70b9b84..ada9fd2 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -101,23 +101,6 @@ export function resolveTypeOptions(category: string | undefined, typeFromFlag: b return typeFromFlag ? TYPE_OPTIONS : filtered; } -// When the user's primary local agent (config.agent, persisted by `polylane -// setup`) also exists as a cloud code agent, surface it first in its category -// and pre-highlight it in the picker. Exact id match only โ€” the local -// registry and the integration types share ids where they overlap (cursor). -export function prioritizeCodeAgent( - options: typeof TYPE_OPTIONS, - localAgent: string | undefined -): { options: typeof TYPE_OPTIONS; initialValue: ConnectableType | undefined } { - const idx = options.findIndex((o) => o.category === 'code-agent' && o.value === localAgent); - if (idx < 0) return { options, initialValue: undefined }; - const first = options.findIndex((o) => o.category === 'code-agent'); - const reordered = [...options]; - const [own] = reordered.splice(idx, 1); - reordered.splice(first, 0, { ...own!, hint: own!.hint.replace(/coding agent$/, 'your coding agent') }); - return { options: reordered, initialValue: own!.value }; -} - // Same site list the console offers; the flag accepts any value so orgs on // sites not listed here (e.g. newer regions) are not locked out. const DATADOG_SITES = [ @@ -798,10 +781,7 @@ export const integrationConnectCommand: Command = { const typeFromFlag = getArgString(args, 'type') !== undefined; const category = getArgString(args, 'category'); // --type always wins: the category filter only narrows the picker. - const { options: typeOptions, initialValue } = prioritizeCodeAgent( - resolveTypeOptions(category, typeFromFlag), - config.agent - ); + const typeOptions = resolveTypeOptions(category, typeFromFlag); if (shouldOfferCodeAgent(category, typeFromFlag, isInteractive(config.nonInteractive))) { note( @@ -840,8 +820,7 @@ export const integrationConnectCommand: Command = { { nonInteractive: config.nonInteractive }, 'Which integration do you want to connect?', typeOptions, - 'Cancel', - initialValue + 'Cancel' ); if (type === BACK) break; const outcome = await connectType(config, api, args, workspaceId, type, noBrowser); diff --git a/src/commands/map.ts b/src/commands/map.ts index 34cfc1a..71eee2e 100644 --- a/src/commands/map.ts +++ b/src/commands/map.ts @@ -146,7 +146,7 @@ export const mapCommand: Command = { options: [ { flag: '--agent ', - description: 'Coding agent to run the map (defaults to your configured agent)', + description: 'Coding agent to run the map (defaults to an installed agent)', type: 'string', }, ], @@ -164,13 +164,9 @@ export const mapCommand: Command = { ); } - // Resolve the primary agent: explicit flag > configured choice > the only - // installed agent > interactive pick among installed agents. - let primary = requestedId - ? agentById(requestedId) - : config.agent - ? agentById(config.agent) - : undefined; + // Resolve the primary agent: explicit flag > the only installed agent > + // interactive pick among installed agents. + let primary = requestedId ? agentById(requestedId) : undefined; if (!primary && installed.length === 1) { primary = installed[0]; } else if (!primary && installed.length > 1 && isInteractive(config.nonInteractive)) { diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 5086dee..8219b84 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -5,15 +5,11 @@ import type { Config } from '../config/schema'; import { tryResolveCredential } from '../auth/resolver'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; -import { isInteractive } from '../utils/env'; -import { promptSelect } from '../utils/prompt'; -import { writeConfigFile } from '../config/loader'; import { getArgArray, getArgBoolean } from './helpers'; -import { AGENTS, type AgentSetup, type WriteAction, type WriteOutcome } from '../agents/registry'; +import { AGENTS, type WriteAction, type WriteOutcome } from '../agents/registry'; -// The registry (agent table + config writers) lives in src/agents/registry.ts -// so the config loader can validate the stored agent id without importing a -// command module; re-exported here because this was its original home. +// The registry (agent table + config writers) lives in src/agents/registry.ts; +// re-exported here because this was its original home. export { AGENTS, MCP_SERVER_NAME, @@ -38,49 +34,6 @@ const ACTION_LABEL: Record = { skipped: 'skipped', }; -export type PrimaryAgentDecision = - | { kind: 'keep' } - | { kind: 'persist'; id: string } - | { kind: 'prompt'; candidates: AgentSetup[] }; - -// The primary agent is the one downstream handoffs address ("open and -// ask ..."); wiring is unaffected โ€” every selected agent gets configured. -export function decidePrimaryAgent( - stored: string | undefined, - selected: AgentSetup[], - interactive: boolean -): PrimaryAgentDecision { - if (stored !== undefined) return { kind: 'keep' }; - if (selected.length === 0) return { kind: 'keep' }; - if (selected.length === 1) return { kind: 'persist', id: selected[0]!.id }; - if (interactive) return { kind: 'prompt', candidates: selected }; - return { kind: 'keep' }; -} - -async function settlePrimaryAgent( - config: Config, - selected: AgentSetup[], - say: (line: string) => void -): Promise { - if (config.dryRun) return; - const decision = decidePrimaryAgent(config.agent, selected, isInteractive(config.nonInteractive)); - if (decision.kind === 'keep') return; - - let id: string; - if (decision.kind === 'persist') { - id = decision.id; - } else { - id = await promptSelect( - { nonInteractive: config.nonInteractive }, - 'Which coding agent do you mainly use?', - decision.candidates.map((a) => ({ value: a.id, label: a.name })), - ); - } - writeConfigFile({ agent: id }); - const name = selected.find((a) => a.id === id)?.name ?? id; - say(`Primary coding agent: ${name} (change with \`polylane config set --key agent --value \`)`); -} - export const setupCommand: Command = { name: 'setup', description: 'Wire the CLI into coding agents (agent skill + MCP server)', @@ -162,8 +115,6 @@ export const setupCommand: Command = { } } - await settlePrimaryAgent(config, selected, say); - const credential = await tryResolveCredential(config); if (credential) { say('Signed in.'); diff --git a/src/config/loader.ts b/src/config/loader.ts index 04089b8..f3565c8 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -13,7 +13,6 @@ import { import type { GlobalFlags, OutputFormat } from '../types/flags'; import { readJsonFile, writeJsonFile } from '../utils/fs'; import { isStdoutTTY } from '../utils/env'; -import { isAgentId } from '../agents/registry'; export function loadConfigFile(): RawConfig | null { return readJsonFile(CONFIG_FILE); @@ -74,12 +73,6 @@ export function loadConfig(flags: GlobalFlags): Config { const workspaceId = flags.workspace ?? env.POLYLANE_WORKSPACE_ID ?? file.workspace_id; if (workspaceId !== undefined) validateWorkspaceId(workspaceId); - // Primary coding agent. Unknown ids are dropped rather than thrown so a - // stale stored value (e.g. an id removed from the registry) never bricks - // every invocation; `config set --key agent` is where strict validation happens. - const agentRaw = env.POLYLANE_AGENT ?? file.agent; - const agent = agentRaw !== undefined && isAgentId(agentRaw) ? agentRaw : undefined; - const timeout = flags.timeout ?? parseEnvNumber(env.POLYLANE_TIMEOUT) ?? file.timeout ?? DEFAULT_TIMEOUT; validateTimeout(timeout); @@ -119,7 +112,6 @@ export function loadConfig(flags: GlobalFlags): Config { apiKey, domain, workspaceId, - agent, output, timeout, verbose, diff --git a/src/config/schema.ts b/src/config/schema.ts index a7bc762..b4c0dab 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -6,8 +6,6 @@ export interface Config { apiKey?: string; domain: string; workspaceId?: string; - /** Primary coding agent id (an AGENTS registry id, e.g. "claude", "cursor"). */ - agent?: string; output: OutputFormat; timeout: number; verbose: boolean; @@ -26,7 +24,6 @@ export interface RawConfig { api_key?: string; domain?: string; workspace_id?: string; - agent?: string; output?: OutputFormat; timeout?: number; telemetry?: boolean; diff --git a/test/config.test.ts b/test/config.test.ts index 9dd8483..1e578e9 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -2,7 +2,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { validateDomain, validateOutput, validateTimeout, validateWorkspaceId } from '../src/config/schema'; import { CLIError } from '../src/errors/base'; -import { AGENT_IDS, validateAgentId } from '../src/agents/registry'; describe('validateDomain', () => { it('accepts valid hostnames', () => { @@ -55,14 +54,3 @@ describe('validateWorkspaceId', () => { assert.throws(() => validateWorkspaceId('acc_rii32455qptezc7467usm3f3hq31qkwp'), CLIError); }); }); - -describe('validateAgentId', () => { - it('accepts every registry id', () => { - for (const id of AGENT_IDS) validateAgentId(id); - }); - - it('rejects unknown ids', () => { - assert.throws(() => validateAgentId('not-an-agent')); - assert.throws(() => validateAgentId('')); - }); -}); diff --git a/test/integration-connect-priority.test.ts b/test/integration-connect-priority.test.ts deleted file mode 100644 index 2bc316b..0000000 --- a/test/integration-connect-priority.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; -import { prioritizeCodeAgent, typeOptionsForCategory } from '../src/commands/integration/connect'; - -describe('prioritizeCodeAgent', () => { - it('moves the local agent to the front of the code-agent group', () => { - const all = typeOptionsForCategory(undefined); - const before = all.map((o) => ({ ...o })); - const { options, initialValue } = prioritizeCodeAgent(all, 'cursor'); - assert.deepEqual(all, before); - const values = options.map((o) => o.value); - assert.equal(initialValue, 'cursor'); - assert.equal(values.indexOf('cursor'), values.indexOf('devin') - 1); - assert.equal(values[0], 'github'); - assert.equal(options.find((o) => o.value === 'cursor')?.hint, 'API key ยท your coding agent'); - }); - - it('pre-highlights the local agent in a narrowed picker', () => { - const { options, initialValue } = prioritizeCodeAgent(typeOptionsForCategory('code-agent'), 'cursor'); - assert.equal(options[0]?.value, 'cursor'); - assert.equal(initialValue, 'cursor'); - }); - - it('keeps grouping intact', () => { - const { options } = prioritizeCodeAgent(typeOptionsForCategory(undefined), 'cursor'); - const categories = options.map((o) => o.category); - assert.deepEqual([...new Set(categories)], ['git', 'communication', 'observability', 'code-agent', 'protocol']); - assert.equal(options.length, typeOptionsForCategory(undefined).length); - }); - - it('is a no-op when the local agent has no cloud counterpart', () => { - const all = typeOptionsForCategory(undefined); - for (const agent of [undefined, 'claude', 'zed']) { - const { options, initialValue } = prioritizeCodeAgent(all, agent); - assert.deepEqual(options, all); - assert.equal(initialValue, undefined); - } - }); -}); diff --git a/test/loader.test.ts b/test/loader.test.ts index 5618b70..e40670d 100644 --- a/test/loader.test.ts +++ b/test/loader.test.ts @@ -1,8 +1,22 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; +import { describe, it, beforeEach, afterEach, after } from 'node:test'; import assert from 'node:assert/strict'; -import { loadConfig } from '../src/config/loader'; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import type { GlobalFlags } from '../src/types/flags'; +// Point HOME at a temp dir before importing any source module, so the loader +// reads this test's config file instead of the developer's real +// ~/.polylane/config.json. Same pattern as signup.test.ts. +const tempHome = mkdtempSync(join(tmpdir(), 'polylane-loader-test-')); +process.env.HOME = tempHome; +after(() => rmSync(tempHome, { recursive: true, force: true })); + +const { loadConfig } = await import('../src/config/loader'); + +const configDir = join(tempHome, '.polylane'); +const configFile = join(configDir, 'config.json'); + describe('loadConfig', () => { const originalEnv = { ...process.env }; @@ -13,8 +27,8 @@ describe('loadConfig', () => { delete process.env.POLYLANE_TIMEOUT; delete process.env.POLYLANE_OUTPUT; delete process.env.POLYLANE_VERBOSE; - delete process.env.POLYLANE_AGENT; delete process.env.POLYLANE_HINTS; + rmSync(configFile, { force: true }); }); afterEach(() => { @@ -49,16 +63,17 @@ describe('loadConfig', () => { assert.equal(config.verbose, true); }); - it('reads the primary agent from env', () => { - process.env.POLYLANE_AGENT = 'cursor'; - const config = loadConfig({} as GlobalFlags); - assert.equal(config.agent, 'cursor'); - }); - - it('drops an unknown agent id instead of throwing', () => { - process.env.POLYLANE_AGENT = 'not-an-agent'; + it('silently tolerates unknown fields in the config file (e.g. a legacy agent key)', () => { + // Older CLI versions persisted a primary coding agent choice; existing + // config files still carry it. It must be ignored, never an error. + mkdirSync(configDir, { recursive: true }); + writeFileSync( + configFile, + JSON.stringify({ domain: 'api.legacy.example.com', agent: 'cursor', some_future_key: true }) + ); const config = loadConfig({} as GlobalFlags); - assert.equal(config.agent, undefined); + assert.equal(config.domain, 'api.legacy.example.com'); + assert.ok(!('agent' in config)); }); it('hints default on', () => { diff --git a/test/setup.test.ts b/test/setup.test.ts index 96039e8..ee28ad3 100644 --- a/test/setup.test.ts +++ b/test/setup.test.ts @@ -15,7 +15,6 @@ import { vscodeUserDirectory, MCP_SERVER_NAME, MCP_SERVER_URL, - decidePrimaryAgent, } from '../src/commands/setup'; import { SKILL_MD } from '../src/generated/skill'; @@ -643,39 +642,3 @@ describe('agent definitions', () => { assert.equal(mcp.action, 'skipped'); }); }); - -describe('decidePrimaryAgent', () => { - const byId = (id: string) => { - const found = AGENTS.find((a) => a.id === id); - assert.ok(found); - return found; - }; - - it('keeps an existing stored choice', () => { - const decision = decidePrimaryAgent('claude', [byId('claude'), byId('cursor')], true); - assert.deepEqual(decision, { kind: 'keep' }); - }); - - it('does nothing when no agents are selected', () => { - const decision = decidePrimaryAgent(undefined, [], true); - assert.deepEqual(decision, { kind: 'keep' }); - }); - - it('persists silently when exactly one agent is in play', () => { - const decision = decidePrimaryAgent(undefined, [byId('codex')], false); - assert.deepEqual(decision, { kind: 'persist', id: 'codex' }); - }); - - it('prompts among the selected agents when several are detected interactively', () => { - const candidates = [byId('claude'), byId('cursor'), byId('gemini')]; - const decision = decidePrimaryAgent(undefined, candidates, true); - assert.equal(decision.kind, 'prompt'); - assert.ok(decision.kind === 'prompt'); - assert.deepEqual(decision.candidates.map((a) => a.id), ['claude', 'cursor', 'gemini']); - }); - - it('does not prompt outside an interactive terminal', () => { - const decision = decidePrimaryAgent(undefined, [byId('claude'), byId('cursor')], false); - assert.deepEqual(decision, { kind: 'keep' }); - }); -});