From 6edee970fb40d8abea1a351b35cdf840a8c189f1 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 30 Apr 2026 11:51:00 +0200 Subject: [PATCH] Let gx open the launcher from the repo home The interactive agents flow needs a usable home state before a task string exists, so panel mode now accepts an empty task only when it can collect the task from the terminal panel. Constraint: Scripted and non-TTY dry-run behavior must stay stable for automation. Rejected: Requiring a task before panel launch | keeps the old dead-end flow for interactive users. Confidence: high Scope-risk: moderate Directive: Keep empty-task acceptance limited to panel mode; do not relax scripted launch validation without tests. Tested: node --test test/agents-selection-panel.test.js test/agents-start-dry-run.test.js test/cli-args-dispatch.test.js Tested: openspec validate agent-codex-gx-dmux-home-launcher-2026-04-30-11-31 --type change --strict Not-tested: Full repository test suite --- .../.openspec.yaml | 2 + .../proposal.md | 16 +++ .../specs/agents-interactive-launcher/spec.md | 34 ++++++ .../tasks.md | 32 +++++ src/agents/selection-panel.js | 114 +++++++++++++++--- src/agents/start.js | 21 +++- src/cli/args.js | 9 +- src/cli/main.js | 17 ++- test/agents-selection-panel.test.js | 28 +++++ test/agents-start-dry-run.test.js | 70 +++++++++++ test/cli-args-dispatch.test.js | 26 ++++ 11 files changed, 349 insertions(+), 20 deletions(-) create mode 100644 openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/.openspec.yaml create mode 100644 openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/proposal.md create mode 100644 openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/specs/agents-interactive-launcher/spec.md create mode 100644 openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/tasks.md diff --git a/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/.openspec.yaml b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/.openspec.yaml new file mode 100644 index 00000000..d6609ca5 --- /dev/null +++ b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/.openspec.yaml @@ -0,0 +1,2 @@ +tier: T2 +change: agent-codex-gx-dmux-home-launcher-2026-04-30-11-31 diff --git a/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/proposal.md b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/proposal.md new file mode 100644 index 00000000..b897c4d6 --- /dev/null +++ b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/proposal.md @@ -0,0 +1,16 @@ +# gx dmux home launcher task prompt + +## Why + +Plain `gx` and `gx agents start --panel` should feel like opening dmux first: show the GitGuardex home launcher immediately, then collect the task inside that launcher. The current parser and panel controller require a task before the panel can open, so the operator has to provide text before seeing the home screen. + +## What Changes + +- Allow `gx agents start --panel` to start without a task when an interactive panel or panel dry-run is requested. +- Put empty-task panels into a task-input mode where printable keys build the task and Enter launches. +- Route plain interactive `gx` to the same home panel instead of status output. +- Preserve existing dry-run, multi-agent, claims, and non-panel start behavior. + +## Impact + +The change is isolated to agent launcher parsing, panel state handling, and focused CLI tests. It does not alter branch creation, locks, session metadata, finish flow, or non-panel agent startup. diff --git a/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/specs/agents-interactive-launcher/spec.md b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/specs/agents-interactive-launcher/spec.md new file mode 100644 index 00000000..d9e8c83f --- /dev/null +++ b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/specs/agents-interactive-launcher/spec.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: Empty panel prompts for task before launch + +`gx agents start --panel` SHALL allow the panel to open without a task and collect the task inside the launcher before creating any agent lane. + +#### Scenario: TTY panel starts in task input mode + +- **WHEN** an operator runs `gx agents start --panel` in a TTY +- **THEN** the GitGuardex launcher SHALL render before any branch/worktree is created +- **AND** the launcher SHALL show a task input prompt +- **AND** printable keys SHALL update the task shown by the launcher +- **AND** Enter SHALL launch only after the task is non-empty. + +#### Scenario: scripted panel dry-run renders home without task plans + +- **WHEN** `gx agents start --panel --dry-run` runs without a TTY and without a task +- **THEN** the output SHALL render the GitGuardex home panel +- **AND** the output SHALL not render dry-run branch plans until a task exists. + +### Requirement: Plain gx opens the interactive launcher home + +Plain interactive `gx` SHALL open the same GitGuardex home launcher instead of status output. + +#### Scenario: no-argument interactive gx shows home launcher + +- **WHEN** an operator runs `gx` with no arguments in a TTY +- **THEN** the command SHALL open the interactive GitGuardex launcher +- **AND** the launcher SHALL ask for the task before launch. + +#### Scenario: non-interactive gx keeps status behavior + +- **WHEN** `gx` runs with no arguments outside a TTY +- **THEN** the command SHALL keep the existing status behavior. diff --git a/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/tasks.md b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/tasks.md new file mode 100644 index 00000000..355234dc --- /dev/null +++ b/openspec/changes/agent-codex-gx-dmux-home-launcher-2026-04-30-11-31/tasks.md @@ -0,0 +1,32 @@ +# Tasks + +## 1. Spec + +- [x] 1.1 Capture empty-task launcher behavior. + +## 2. Tests + +- [x] 2.1 Cover task input reducer behavior for empty panels. +- [x] 2.2 Cover `gx agents start --panel --dry-run` rendering home without a task. +- [x] 2.3 Cover interactive empty-panel task entry before launch. +- [x] 2.4 Cover parser acceptance for empty `--panel` starts. + +## 3. Implementation + +- [x] 3.1 Allow empty task only for panel starts. +- [x] 3.2 Add task-input mode to the dmux-style panel controller. +- [x] 3.3 Route plain interactive `gx` to the launcher home. + +## 4. Verification + +- [x] 4.1 Run focused Node tests. + - Evidence: `node --test test/agents-selection-panel.test.js test/agents-start-dry-run.test.js test/agents-start.test.js test/cli-args-dispatch.test.js test/agents-start-claims.test.js` passed (`33/33`). +- [x] 4.2 Validate OpenSpec change. + - Evidence: `openspec validate agent-codex-gx-dmux-home-launcher-2026-04-30-11-31 --type change --strict` passed. +- [x] 4.3 Smoke `gx agents start --panel --dry-run`. + - Evidence: `node bin/multiagent-safety.js agents --target /home/deadpool/Documents/recodee/gitguardex start --panel --dry-run` rendered the GitGuardex home panel with task input and no dry-run branch plans. + +## 5. Cleanup + +- [ ] 5.1 Run the finish pipeline: `gx branch finish --branch agent/codex/gx-dmux-home-launcher-2026-04-30-11-31 --base main --via-pr --wait-for-merge --cleanup`. +- [ ] 5.2 Record PR URL, final `MERGED` state, and sandbox cleanup evidence. diff --git a/src/agents/selection-panel.js b/src/agents/selection-panel.js index c13db444..6006d69a 100644 --- a/src/agents/selection-panel.js +++ b/src/agents/selection-panel.js @@ -103,14 +103,18 @@ function createAgentSelectionPanelState(options = {}) { const selections = normalizeAgentSelections({ ...options, maxSelected }); const counts = countsFromSelections(selections); const firstSelectedIndex = definitions.findIndex((agent) => (counts[agent.id] || 0) > 0); + const task = String(options.task || ''); return { - task: options.task || '', + task, base: options.base || '', claims: Array.isArray(options.claims) ? [...options.claims] : [], maxSelected, focusIndex: firstSelectedIndex >= 0 ? firstSelectedIndex : 0, counts, - message: '', + taskInputActive: Object.prototype.hasOwnProperty.call(options, 'taskInputActive') + ? Boolean(options.taskInputActive) + : !task.trim(), + message: options.message || '', }; } @@ -144,17 +148,57 @@ function withCount(state, agentId, count, message = '') { }; } -function normalizePanelKey(value) { +function rawPanelKey(value) { if (!value) return ''; - const raw = Buffer.isBuffer(value) ? value.toString('utf8') : String(value); + return Buffer.isBuffer(value) ? value.toString('utf8') : String(value); +} + +function normalizePanelKey(value) { + const raw = rawPanelKey(value); if (raw === '\u0003') return 'ctrl-c'; + if (raw === '\u0015') return 'ctrl-u'; if (raw === '\u001b') return 'escape'; if (raw === '\r' || raw === '\n') return 'enter'; + if (raw === '\u007f' || raw === '\b') return 'backspace'; if (raw === '\u001b[A') return 'up'; if (raw === '\u001b[B') return 'down'; return raw.toLowerCase(); } +function printableTaskInput(value) { + const raw = rawPanelKey(value); + if (raw.length !== 1) return ''; + const code = raw.charCodeAt(0); + if (code < 32 || code > 126) return ''; + return raw; +} + +function taskLaunchState(state) { + const task = String(state.task || '').trim(); + if (!task) { + return { + state: { + ...state, + taskInputActive: true, + message: 'Type a task before launch.', + }, + action: 'render', + }; + } + if (selectedCountFromPanelState(state) <= 0) { + return { state: { ...state, message: 'Select at least one agent before launch.' }, action: 'render' }; + } + return { + state: { + ...state, + task, + taskInputActive: false, + message: '', + }, + action: 'launch', + }; +} + function applyAgentSelectionKey(state = {}, rawKey) { const definitions = getAgentDefinitions(); const current = { @@ -165,14 +209,45 @@ function applyAgentSelectionKey(state = {}, rawKey) { }; const key = normalizePanelKey(rawKey); - if (key === 'ctrl-c' || key === 'escape' || key === 'q') { + if (key === 'ctrl-c' || key === 'escape' || (!current.taskInputActive && key === 'q')) { return { state: current, action: 'cancel' }; } - if (key === 'enter' || key === 'n') { - if (selectedCountFromPanelState(current) <= 0) { - return { state: { ...current, message: 'Select at least one agent before launch.' }, action: 'render' }; + + if (current.taskInputActive) { + if (key === 'enter') { + return taskLaunchState(current); + } + if (key === 'backspace') { + return { + state: { + ...current, + task: String(current.task || '').slice(0, -1), + message: '', + }, + action: 'render', + }; } - return { state: current, action: 'launch' }; + if (key === 'ctrl-u') { + return { state: { ...current, task: '', message: '' }, action: 'render' }; + } + + const input = printableTaskInput(rawKey); + if (input) { + return { + state: { + ...current, + task: `${current.task || ''}${input}`, + message: '', + }, + action: 'render', + }; + } + + return { state: current, action: 'render' }; + } + + if (key === 'enter' || key === 'n') { + return taskLaunchState(current); } if (key === 'up' || key === 'k') { return { @@ -289,6 +364,7 @@ function renderSidebarRows(options, selections, definitions, width, height) { ? options.claims.join(', ') : 'none'; const topBars = '█'.repeat(Math.max(0, width - 13)); + const taskText = options.task ? options.task : (options.taskInputActive ? '_' : '-'); const rows = [ `─ gx ${'─'.repeat(Math.max(0, width - 5))}`, `▦ gitguardex ${topBars}`.slice(0, width), @@ -306,11 +382,13 @@ function renderSidebarRows(options, selections, definitions, width, height) { }), '', ' Settings', - ` task: ${options.task || '-'}`, + ` task: ${taskText}`, ` base: ${options.base || 'current branch'}`, ` Codex accounts: ${codexAccounts}`, ` claims: ${claims}`, - options.message ? ` status: ${options.message}` : '', + options.message + ? ` status: ${options.message}` + : (options.taskInputActive ? ' status: Type task, then Enter.' : ''), ]; while (rows.length < height - 5) { @@ -345,7 +423,9 @@ function renderLogoCardRows(options, selections, width) { centerLine('gitguardex', innerWidth), centerLine('AI developer agent guardrail multiplexer', innerWidth), centerLine(`Select Agent(s) · Selected: ${total}/${maxSelected}`, innerWidth), - centerLine('Press [n] or Enter to create a new agent', innerWidth), + centerLine(options.taskInputActive + ? 'Type task, then press Enter' + : 'Press [n] or Enter to create a new agent', innerWidth), ]; return [ @@ -393,7 +473,10 @@ function renderDmuxAgentSelectionPanel(options = {}) { const mainWidth = Math.max(42, width - sidebarWidth - 1); const sidebar = renderSidebarRows({ ...options, maxSelected }, selections, definitions, sidebarWidth, height); const main = renderMainRows({ ...options, maxSelected }, selections, mainWidth, height); - const keyLine = padLine(' ↑/↓ navigate · Space toggle · +/- Codex accounts · [n]/Enter launch · ESC cancel ', width); + const keyText = options.taskInputActive + ? ' Type task · Backspace edit · Enter launch · ESC cancel ' + : ' ↑/↓ navigate · Space toggle · +/- Codex accounts · [n]/Enter launch · ESC cancel '; + const keyLine = padLine(keyText, width); const lines = []; for (let index = 0; index < height; index += 1) { @@ -438,7 +521,9 @@ function renderAgentSelectionPanel(options = {}) { `claims: ${claims}`, options.message ? `status: ${options.message}` : null, '', - '↑/↓ navigate · Space toggle · +/- Codex accounts · [n]/Enter launch · ESC cancel', + options.taskInputActive + ? 'Type task · Backspace edit · Enter launch · ESC cancel' + : '↑/↓ navigate · Space toggle · +/- Codex accounts · [n]/Enter launch · ESC cancel', ].filter((row) => row !== null); return `${framePanel('Select Agent(s)', rows)}\n`; } @@ -452,6 +537,7 @@ function renderInteractiveAgentSelectionPanel(state = {}, options = {}) { selections: selectionsFromPanelState(state), focusedAgentId: focusedAgent(state)?.id, message: state.message, + taskInputActive: state.taskInputActive, ...options, }); } diff --git a/src/agents/start.js b/src/agents/start.js index 7f2ddc23..0fb42901 100644 --- a/src/agents/start.js +++ b/src/agents/start.js @@ -157,6 +157,17 @@ function renderDryRunPlan(plan) { } function dryRunStart(options, repoRoot) { + const hasTask = Boolean(String(options.task || '').trim()); + if (options.panel && !hasTask && !options.json) { + return renderAgentSelectionPanel({ + task: '', + base: options.base, + claims: options.claims, + selections: normalizeAgentSelections(options), + taskInputActive: true, + }); + } + const launchOptions = buildLaunchOptions(options); const plans = launchOptions.map((launchOption) => buildStartPlan(launchOption, repoRoot)); if (options.json) { @@ -196,6 +207,7 @@ function panelOptions(options, state) { const first = selectionsFromPanelState(state)[0]; return { ...options, + task: String(state.task || '').trim(), agent: first ? first.agent.id : options.agent, count: 1, agentSelectionSpecs: specs, @@ -204,6 +216,13 @@ function panelOptions(options, state) { function executePanelSelection(repoRoot, options, state, deps = {}) { const selectedOptions = panelOptions(options, state); + if (!selectedOptions.task) { + return { + status: 1, + stdout: '', + stderr: `[${TOOL_NAME}] Agent launch requires a task.\n`, + }; + } if (selectedOptions.dryRun) { return { status: 0, @@ -222,7 +241,7 @@ function writeStream(stream, value) { } function shouldUseInteractivePanel(options = {}, stdin = process.stdin, stdout = process.stdout) { - return Boolean(options.panel && options.task && stdin && stdin.isTTY && stdout && stdout.isTTY); + return Boolean(options.panel && stdin && stdin.isTTY && stdout && stdout.isTTY); } function startInteractiveAgentPanel(repoRoot, options, deps = {}) { diff --git a/src/cli/args.js b/src/cli/args.js index ed965ab0..d639c234 100644 --- a/src/cli/args.js +++ b/src/cli/args.js @@ -515,17 +515,18 @@ function parseAgentsArgs(rawArgs) { if (options.dryRun && !['start', 'cleanup-sessions'].includes(options.subcommand)) { throw new Error('--dry-run is only supported with `gx agents start|cleanup-sessions`'); } - if (options.subcommand === 'start' && options.dryRun && !options.task) { + if (options.subcommand === 'start' && options.dryRun && !options.task && !(options.panel && !options.json)) { throw new Error('gx agents start --dry-run requires a task'); } if ( options.subcommand === 'start' && !options.task && - (options.agentSelectionSpecs.length > 0 || options.count !== 1 || options.panel) + !options.panel && + (options.agentSelectionSpecs.length > 0 || options.count !== 1) ) { - throw new Error('gx agents start --agents|--count|--panel requires a task'); + throw new Error('gx agents start --agents|--count requires a task'); } - if (options.claims.length > 0 && !options.task) { + if (options.claims.length > 0 && !options.task && !options.panel) { throw new Error('gx agents start --claim requires a task'); } if (['files', 'diff', 'locks'].includes(options.subcommand) && !options.branch) { diff --git a/src/cli/main.js b/src/cli/main.js index 6a32ef46..ad835db4 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -2673,7 +2673,7 @@ function agents(rawArgs) { } if (options.subcommand === 'start') { - if (options.task && agentsStart.shouldUseInteractivePanel(options, process.stdin, process.stdout)) { + if (agentsStart.shouldUseInteractivePanel(options, process.stdin, process.stdout)) { agentsStart.startInteractiveAgentPanel(repoRoot, options, { onDone(result) { process.exitCode = result.status; @@ -2687,6 +2687,11 @@ function agents(rawArgs) { process.exitCode = 0; return; } + if (options.panel && !options.task) { + process.stderr.write('[gitguardex] gx agents start --panel requires an interactive terminal when no task is provided.\n'); + process.exitCode = 1; + return; + } if (options.task) { const result = agentsStart.startAgentLane(repoRoot, options); if (result.stdout) process.stdout.write(result.stdout); @@ -3744,6 +3749,16 @@ async function main() { const args = process.argv.slice(2); if (args.length === 0) { + if (isInteractiveTerminal()) { + const options = parseAgentsArgs(['start', '--panel']); + const repoRoot = resolveRepoRoot(options.target); + agentsStart.startInteractiveAgentPanel(repoRoot, options, { + onDone(result) { + process.exitCode = result.status; + }, + }); + return; + } toolchainModule.maybeSelfUpdateBeforeStatus(); toolchainModule.maybeOpenSpecUpdateBeforeStatus(); const statusPayload = status([]); diff --git a/test/agents-selection-panel.test.js b/test/agents-selection-panel.test.js index c9822381..55ced477 100644 --- a/test/agents-selection-panel.test.js +++ b/test/agents-selection-panel.test.js @@ -63,6 +63,34 @@ test('renderAgentSelectionPanel shows a dmux-style GitGuardex shell', () => { assert.match(blueOutput, /\x1b\[94m/); }); +test('empty interactive panel captures a task before launch', () => { + let state = createAgentSelectionPanelState({ + task: '', + base: 'main', + agentSelectionSpecs: ['codex'], + }); + + assert.equal(state.taskInputActive, true); + assert.match(renderInteractiveAgentSelectionPanel(state), /Type task, then press Enter/); + assert.match(renderInteractiveAgentSelectionPanel(state), /task: _/); + + let next = applyAgentSelectionKey(state, 'n'); + assert.equal(next.action, 'render'); + assert.equal(next.state.task, 'n'); + state = next.state; + + state = applyAgentSelectionKey(state, 'e').state; + state = applyAgentSelectionKey(state, 'q').state; + state = applyAgentSelectionKey(state, '\u007f').state; + state = applyAgentSelectionKey(state, 'w').state; + assert.equal(state.task, 'new'); + + next = applyAgentSelectionKey(state, '\r'); + assert.equal(next.action, 'launch'); + assert.equal(next.state.task, 'new'); + assert.equal(next.state.taskInputActive, false); +}); + test('interactive panel keys move focus, toggle agents, and adjust codex accounts', () => { let state = createAgentSelectionPanelState({ task: 'repair auth', diff --git a/test/agents-start-dry-run.test.js b/test/agents-start-dry-run.test.js index 4695458d..77420cf4 100644 --- a/test/agents-start-dry-run.test.js +++ b/test/agents-start-dry-run.test.js @@ -106,6 +106,23 @@ test('gx agents start dry-run renders a terminal panel for multiple codex accoun assert.notEqual(branchCheck.status, 0, 'dry-run must not create multi-account branches'); }); +test('gx agents start --panel --dry-run can render the home panel before a task exists', () => { + const repoDir = initRepo(); + seedCommit(repoDir); + + const result = runNode( + ['agents', 'start', '--panel', '--dry-run'], + repoDir, + ); + + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /Select Agent\(s\)/); + assert.match(result.stdout, /gitguardex/); + assert.match(result.stdout, /Type task, then press Enter/); + assert.match(result.stdout, /task: _/); + assert.doesNotMatch(result.stdout, /Agents start dry-run:/); +}); + test('gx agents start --dry-run --json emits Colony-ready launch plan', () => { const repoDir = initRepo(); seedCommit(repoDir); @@ -228,3 +245,56 @@ test('interactive launcher panel handles keys before emitting dry-run plans', () assert.match(output, /branch: agent\/codex\/fix-auth-tests-codex-01-/); assert.match(output, /branch: agent\/codex\/fix-auth-tests-codex-02-/); }); + +test('interactive launcher panel asks for a task when opened empty', () => { + const input = new FakeInput(); + const stdout = { + isTTY: true, + columns: 120, + rows: 32, + chunks: [], + write(chunk) { + this.chunks.push(String(chunk)); + }, + }; + const stderr = { + chunks: [], + write(chunk) { + this.chunks.push(String(chunk)); + }, + }; + let done = null; + + const controller = startInteractiveAgentPanel('/repo', { + task: '', + agent: 'codex', + base: 'main', + count: 1, + panel: true, + dryRun: true, + claims: [], + }, { + stdin: input, + stdout, + stderr, + onDone(result) { + done = result; + }, + }); + + assert.equal(input.resumed, true); + assert.match(stdout.chunks.join(''), /Type task, then press Enter/); + + for (const key of ['f', 'i', 'x', ' ', 'a', 'u', 't', 'h']) { + controller.dispatch(key); + } + controller.dispatch('\r'); + + assert.deepEqual(input.rawModes, [true, false]); + assert.equal(done.status, 0); + assert.equal(stderr.chunks.join(''), ''); + const output = stdout.chunks.join(''); + assert.match(output, /task: fix auth/); + assert.match(output, /branch: agent\/codex\/fix-auth-/); + assert.match(output, /launch: cd '.*' && 'codex' 'fix auth'/); +}); diff --git a/test/cli-args-dispatch.test.js b/test/cli-args-dispatch.test.js index aa6f0e53..8620e33b 100644 --- a/test/cli-args-dispatch.test.js +++ b/test/cli-args-dispatch.test.js @@ -171,6 +171,32 @@ test('parseAgentsArgs applies interval overrides and validates the subcommand', assert.equal(panelOptions.agent, 'codex'); assert.equal(panelOptions.count, 3); assert.deepEqual(panelOptions.agentSelectionSpecs, ['codex:2,claude']); + + const emptyPanelOptions = parseAgentsArgs([ + 'start', + '--panel', + '--codex-accounts', + '3', + '--claim', + 'src/auth.js', + '--dry-run', + ]); + + assert.equal(emptyPanelOptions.task, ''); + assert.equal(emptyPanelOptions.panel, true); + assert.equal(emptyPanelOptions.agent, 'codex'); + assert.equal(emptyPanelOptions.count, 3); + assert.deepEqual(emptyPanelOptions.claims, ['src/auth.js']); + assert.equal(emptyPanelOptions.dryRun, true); + + assert.throws( + () => parseAgentsArgs(['start', '--dry-run']), + /gx agents start --dry-run requires a task/, + ); + assert.throws( + () => parseAgentsArgs(['start', '--panel', '--dry-run', '--json']), + /gx agents start --dry-run requires a task/, + ); }); test('parseReportArgs accepts the session-severity flag set', () => {