From 5297251e0049b8d7ff940a10b326d5e6346ee2a0 Mon Sep 17 00:00:00 2001 From: insleker Date: Tue, 14 Apr 2026 14:44:30 +0800 Subject: [PATCH] feat: ask CLI respond in plain text rather than json --- examples/README.md | 12 +++- examples/simple-gitea.yaml | 2 +- examples/simple-github.yaml | 7 +- examples/simple-gitlab.yaml | 2 +- examples/simple-openproject.yaml | 2 +- lib/src/config/config.dart | 23 ++++--- test/config/app_config_test.dart | 103 ++++++++++++++++++++++++++---- test/core/cli_responder_test.dart | 2 +- 8 files changed, 126 insertions(+), 27 deletions(-) diff --git a/examples/README.md b/examples/README.md index ba3c370..ed30bb2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -80,7 +80,17 @@ Start from [`examples/simple-github.yaml`](simple-github.yaml) for GitHub, This repo uses `dataDir`, `worktreeDir`, and `projects`, and uses `worktreeDir` for bug-verification worktrees. +## responder CLI mode + +Configure responders in plain-text mode. Do not add JSON/output-format flags in +`responders[].args` (for example `--json`, `--json-output`, `--output-format`, +or `-f json`), because `code_work_spawner` owns response-format handling. + +Prompts are passed through stdin via `stdinTemplate` (default: `{{prompt}}`). +In most cases prompt flags are not needed in `responders[].args`, because the +real prompt already comes from stdin. Only add a prompt flag if a specific CLI +fails without it. + ## tea + windows The `tea` CLI has known severe issues on Windows due to lipgloss/v2 query console color. We had bypass it by read config file of `tea` directly, but only token login is supported. And need to config a default login through `tea login default `. - diff --git a/examples/simple-gitea.yaml b/examples/simple-gitea.yaml index 217812b..c43d0b5 100644 --- a/examples/simple-gitea.yaml +++ b/examples/simple-gitea.yaml @@ -13,7 +13,7 @@ defaults: responders: - id: codex command: codex - args: ["exec", "--json"] + args: ["exec"] timeout: 10m projects: diff --git a/examples/simple-github.yaml b/examples/simple-github.yaml index dbb14a9..c05116c 100644 --- a/examples/simple-github.yaml +++ b/examples/simple-github.yaml @@ -25,15 +25,16 @@ defaults: # Replace this with a responder available on your machine. - id: codex command: codex - args: ["-s", "danger-full-access", "exec", "--json"] + args: ["-s", "danger-full-access", "exec"] timeout: 10m # - id: opencode # command: opencode - # args: ["run", "-f", "json"] + # args: ["run"] # timeout: 10m # - id: copilot-cli # command: copilot - # args: ["-p", "", "--allow-all", "--autopilot", "--silent"] + # # Prompt text is sent through stdin, so prompt flags are usually unnecessary. + # args: ["--allow-all", "--autopilot", "--silent"] # timeout: 10m # notifications: # # Optional Discord notifications for assistant activity. diff --git a/examples/simple-gitlab.yaml b/examples/simple-gitlab.yaml index 8968b9d..9d95d7f 100644 --- a/examples/simple-gitlab.yaml +++ b/examples/simple-gitlab.yaml @@ -13,7 +13,7 @@ defaults: responders: - id: codex command: codex - args: ["exec", "--json"] + args: ["exec"] timeout: 10m projects: diff --git a/examples/simple-openproject.yaml b/examples/simple-openproject.yaml index d4a50b9..db55379 100644 --- a/examples/simple-openproject.yaml +++ b/examples/simple-openproject.yaml @@ -13,7 +13,7 @@ defaults: responders: - id: codex command: codex - args: ["exec", "--json"] + args: ["exec"] timeout: 10m projects: diff --git a/lib/src/config/config.dart b/lib/src/config/config.dart index 1e9be8d..0e15c8b 100644 --- a/lib/src/config/config.dart +++ b/lib/src/config/config.dart @@ -494,17 +494,24 @@ class CliResponderConfig { return CliResponderConfig( id: id, command: command, - args: _normalizeArgs(document.args ?? const []), + args: _validateArgs(id, document.args ?? const []), env: document.env ?? const {}, stdinTemplate: document.stdinTemplate ?? '{{prompt}}', timeout: _parseDuration(document.timeout ?? '10m'), ); } - static List _normalizeArgs(Iterable args) { - return args - .map((arg) => arg == '--json-output' ? '--json' : arg) - .toList(growable: false); + static List _validateArgs(String id, Iterable args) { + final normalized = args.toList(growable: false); + for (final arg in normalized) { + if (arg.toLowerCase().contains('json')) { + throw FormatException( + 'Responder "$id" has forbidden arg "$arg": ' + 'JSON/output-format args must not be configured in responders.args.', + ); + } + } + return normalized; } } @@ -1082,15 +1089,15 @@ ${_commentBlock(defaultIssueAssistantPrompt, indent: ' # ')} # responders: # - id: codex # command: codex - # args: ["exec", "--json"] + # args: ["exec"] # timeout: 10m # - id: opencode # command: opencode - # args: ["run", "-f", "json"] + # args: ["run"] # timeout: 10m # - id: copilot-cli # command: copilot - # args: ["-p", "", "--allow-all", "--autopilot", "--silent"] + # args: ["--allow-all", "--autopilot", "--silent"] # timeout: 10m # notifications: # - type: discordWebhook diff --git a/test/config/app_config_test.dart b/test/config/app_config_test.dart index a2a71a6..afc531a 100644 --- a/test/config/app_config_test.dart +++ b/test/config/app_config_test.dart @@ -241,13 +241,13 @@ projects: }); /// ```gherkin - /// Scenario: Normalize legacy codex json flag - /// Given an orchestrator config that still uses the legacy codex --json-output flag + /// Scenario: Reject responder args that contain json output flags + /// Given an orchestrator config that configures a responder json output argument /// When loading the app config from disk - /// Then the responder args are normalized to the current codex --json flag + /// Then loading fails with a format exception about forbidden args /// ``` - test('Normalize legacy codex json flag', () async { - // Given an orchestrator config that still uses the legacy codex --json-output flag. + test('Reject responder args that contain json output flags', () async { + // Given an orchestrator config that configures a responder json output argument. final tempDir = await Directory.systemTemp.createTemp('cws-codex-args-'); final configFile = File(p.join(tempDir.path, 'CWS_conf.yaml')); await configFile.writeAsString(''' @@ -263,13 +263,97 @@ projects: path: ${tempDir.path} '''); + // When loading the app config from disk. + final future = AppConfig.load(configFile.path); + + // Then loading fails with a format exception about forbidden args. + await expectLater( + future, + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('forbidden arg "--json-output"'), + ), + ), + ); + }); + + /// ```gherkin + /// Scenario: Reject responder args with mixed-case json tokens + /// Given an orchestrator config that configures a responder argument containing Json in mixed case + /// When loading the app config from disk + /// Then loading fails with a format exception about forbidden args + /// ``` + test('Reject responder args with mixed-case json tokens', () async { + // Given an orchestrator config that configures a responder argument containing Json in mixed case. + final tempDir = await Directory.systemTemp.createTemp( + 'cws-codex-args-case-', + ); + final configFile = File(p.join(tempDir.path, 'CWS_conf.yaml')); + await configFile.writeAsString(''' +defaults: + issueAssistant: + responders: + - id: codex + command: codex + args: ["exec", "--JsonOutput"] +projects: + sample: + repo: owner/sample + path: ${tempDir.path} +'''); + + // When loading the app config from disk. + final future = AppConfig.load(configFile.path); + + // Then loading fails with a format exception about forbidden args. + await expectLater( + future, + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('forbidden arg "--JsonOutput"'), + ), + ), + ); + }); + + /// ```gherkin + /// Scenario: Keep non-format responder args + /// Given an orchestrator config that only sets non-format responder args + /// When loading the app config from disk + /// Then those args remain available to the responder + /// ``` + test('Keep non-format responder args', () async { + // Given an orchestrator config that only sets non-format responder args. + final tempDir = await Directory.systemTemp.createTemp( + 'cws-codex-args-keep-', + ); + final configFile = File(p.join(tempDir.path, 'CWS_conf.yaml')); + await configFile.writeAsString(''' +defaults: + issueAssistant: + responders: + - id: codex + command: codex + args: ["-s", "danger-full-access", "exec", "--allow-all"] +projects: + sample: + repo: owner/sample + path: ${tempDir.path} +'''); + // When loading the app config from disk. final config = await AppConfig.load(configFile.path); - // Then the responder args are normalized to the current codex --json flag. + // Then those args remain available to the responder. expect(config.projects['sample']!.issueAssistant.responders.single.args, [ + '-s', + 'danger-full-access', 'exec', - '--json', + '--allow-all', ]); }); @@ -1223,10 +1307,7 @@ projects: .replaceFirst(' # responders:', ' responders:') .replaceFirst(' # - id: codex', ' - id: codex') .replaceFirst(' # command: codex', ' command: codex') - .replaceFirst( - ' # args: ["exec", "--json"]', - ' args: ["exec", "--json"]', - ) + .replaceFirst(' # args: ["exec"]', ' args: ["exec"]') .replaceFirst(' # timeout: 10m', ' timeout: 10m') .replaceFirst(' # issueAssistant:', ' issueAssistant:') .replaceFirst(' # enabled: true', ' enabled: true') diff --git a/test/core/cli_responder_test.dart b/test/core/cli_responder_test.dart index 48a50c3..e7ea075 100644 --- a/test/core/cli_responder_test.dart +++ b/test/core/cli_responder_test.dart @@ -395,7 +395,7 @@ projects: test('Decode Copilot CLI JSONL assistant messages', () async { // Given a responder command that emits Copilot CLI JSONL events. final sandbox = await Directory.systemTemp.createTemp( - 'cws-copilot-jsonl-', + 'cws-copilot-events-', ); final scriptFile = File(p.join(sandbox.path, 'responder.dart')); await scriptFile.writeAsString('''