feat: ask CLI respond in plain text rather than json

This commit is contained in:
insleker 2026-04-14 14:44:30 +08:00
parent ed94fde9e7
commit 5297251e00
8 changed files with 126 additions and 27 deletions

View File

@ -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 <LOGIN>`.

View File

@ -13,7 +13,7 @@ defaults:
responders:
- id: codex
command: codex
args: ["exec", "--json"]
args: ["exec"]
timeout: 10m
projects:

View File

@ -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.

View File

@ -13,7 +13,7 @@ defaults:
responders:
- id: codex
command: codex
args: ["exec", "--json"]
args: ["exec"]
timeout: 10m
projects:

View File

@ -13,7 +13,7 @@ defaults:
responders:
- id: codex
command: codex
args: ["exec", "--json"]
args: ["exec"]
timeout: 10m
projects:

View File

@ -494,17 +494,24 @@ class CliResponderConfig {
return CliResponderConfig(
id: id,
command: command,
args: _normalizeArgs(document.args ?? const <String>[]),
args: _validateArgs(id, document.args ?? const <String>[]),
env: document.env ?? const <String, String>{},
stdinTemplate: document.stdinTemplate ?? '{{prompt}}',
timeout: _parseDuration(document.timeout ?? '10m'),
);
}
static List<String> _normalizeArgs(Iterable<String> args) {
return args
.map((arg) => arg == '--json-output' ? '--json' : arg)
.toList(growable: false);
static List<String> _validateArgs(String id, Iterable<String> 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

View File

@ -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<FormatException>().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<FormatException>().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')

View File

@ -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('''