feat: ask CLI respond in plain text rather than json
This commit is contained in:
parent
ed94fde9e7
commit
5297251e00
|
|
@ -80,7 +80,17 @@ Start from [`examples/simple-github.yaml`](simple-github.yaml) for GitHub,
|
||||||
This repo uses `dataDir`, `worktreeDir`, and `projects`, and uses
|
This repo uses `dataDir`, `worktreeDir`, and `projects`, and uses
|
||||||
`worktreeDir` for bug-verification worktrees.
|
`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
|
## 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>`.
|
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>`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defaults:
|
||||||
responders:
|
responders:
|
||||||
- id: codex
|
- id: codex
|
||||||
command: codex
|
command: codex
|
||||||
args: ["exec", "--json"]
|
args: ["exec"]
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
|
|
||||||
projects:
|
projects:
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ defaults:
|
||||||
# Replace this with a responder available on your machine.
|
# Replace this with a responder available on your machine.
|
||||||
- id: codex
|
- id: codex
|
||||||
command: codex
|
command: codex
|
||||||
args: ["-s", "danger-full-access", "exec", "--json"]
|
args: ["-s", "danger-full-access", "exec"]
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
# - id: opencode
|
# - id: opencode
|
||||||
# command: opencode
|
# command: opencode
|
||||||
# args: ["run", "-f", "json"]
|
# args: ["run"]
|
||||||
# timeout: 10m
|
# timeout: 10m
|
||||||
# - id: copilot-cli
|
# - id: copilot-cli
|
||||||
# command: copilot
|
# 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
|
# timeout: 10m
|
||||||
# notifications:
|
# notifications:
|
||||||
# # Optional Discord notifications for assistant activity.
|
# # Optional Discord notifications for assistant activity.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defaults:
|
||||||
responders:
|
responders:
|
||||||
- id: codex
|
- id: codex
|
||||||
command: codex
|
command: codex
|
||||||
args: ["exec", "--json"]
|
args: ["exec"]
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
|
|
||||||
projects:
|
projects:
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defaults:
|
||||||
responders:
|
responders:
|
||||||
- id: codex
|
- id: codex
|
||||||
command: codex
|
command: codex
|
||||||
args: ["exec", "--json"]
|
args: ["exec"]
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
|
|
||||||
projects:
|
projects:
|
||||||
|
|
|
||||||
|
|
@ -494,17 +494,24 @@ class CliResponderConfig {
|
||||||
return CliResponderConfig(
|
return CliResponderConfig(
|
||||||
id: id,
|
id: id,
|
||||||
command: command,
|
command: command,
|
||||||
args: _normalizeArgs(document.args ?? const <String>[]),
|
args: _validateArgs(id, document.args ?? const <String>[]),
|
||||||
env: document.env ?? const <String, String>{},
|
env: document.env ?? const <String, String>{},
|
||||||
stdinTemplate: document.stdinTemplate ?? '{{prompt}}',
|
stdinTemplate: document.stdinTemplate ?? '{{prompt}}',
|
||||||
timeout: _parseDuration(document.timeout ?? '10m'),
|
timeout: _parseDuration(document.timeout ?? '10m'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<String> _normalizeArgs(Iterable<String> args) {
|
static List<String> _validateArgs(String id, Iterable<String> args) {
|
||||||
return args
|
final normalized = args.toList(growable: false);
|
||||||
.map((arg) => arg == '--json-output' ? '--json' : arg)
|
for (final arg in normalized) {
|
||||||
.toList(growable: false);
|
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:
|
# responders:
|
||||||
# - id: codex
|
# - id: codex
|
||||||
# command: codex
|
# command: codex
|
||||||
# args: ["exec", "--json"]
|
# args: ["exec"]
|
||||||
# timeout: 10m
|
# timeout: 10m
|
||||||
# - id: opencode
|
# - id: opencode
|
||||||
# command: opencode
|
# command: opencode
|
||||||
# args: ["run", "-f", "json"]
|
# args: ["run"]
|
||||||
# timeout: 10m
|
# timeout: 10m
|
||||||
# - id: copilot-cli
|
# - id: copilot-cli
|
||||||
# command: copilot
|
# command: copilot
|
||||||
# args: ["-p", "", "--allow-all", "--autopilot", "--silent"]
|
# args: ["--allow-all", "--autopilot", "--silent"]
|
||||||
# timeout: 10m
|
# timeout: 10m
|
||||||
# notifications:
|
# notifications:
|
||||||
# - type: discordWebhook
|
# - type: discordWebhook
|
||||||
|
|
|
||||||
|
|
@ -241,13 +241,13 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Normalize legacy codex json flag
|
/// Scenario: Reject responder args that contain json output flags
|
||||||
/// Given an orchestrator config that still uses the legacy codex --json-output flag
|
/// Given an orchestrator config that configures a responder json output argument
|
||||||
/// When loading the app config from disk
|
/// 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 {
|
test('Reject responder args that contain json output flags', () async {
|
||||||
// Given an orchestrator config that still uses the legacy codex --json-output flag.
|
// Given an orchestrator config that configures a responder json output argument.
|
||||||
final tempDir = await Directory.systemTemp.createTemp('cws-codex-args-');
|
final tempDir = await Directory.systemTemp.createTemp('cws-codex-args-');
|
||||||
final configFile = File(p.join(tempDir.path, 'CWS_conf.yaml'));
|
final configFile = File(p.join(tempDir.path, 'CWS_conf.yaml'));
|
||||||
await configFile.writeAsString('''
|
await configFile.writeAsString('''
|
||||||
|
|
@ -263,13 +263,97 @@ projects:
|
||||||
path: ${tempDir.path}
|
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.
|
// When loading the app config from disk.
|
||||||
final config = await AppConfig.load(configFile.path);
|
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, [
|
expect(config.projects['sample']!.issueAssistant.responders.single.args, [
|
||||||
|
'-s',
|
||||||
|
'danger-full-access',
|
||||||
'exec',
|
'exec',
|
||||||
'--json',
|
'--allow-all',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1223,10 +1307,7 @@ projects:
|
||||||
.replaceFirst(' # responders:', ' responders:')
|
.replaceFirst(' # responders:', ' responders:')
|
||||||
.replaceFirst(' # - id: codex', ' - id: codex')
|
.replaceFirst(' # - id: codex', ' - id: codex')
|
||||||
.replaceFirst(' # command: codex', ' command: codex')
|
.replaceFirst(' # command: codex', ' command: codex')
|
||||||
.replaceFirst(
|
.replaceFirst(' # args: ["exec"]', ' args: ["exec"]')
|
||||||
' # args: ["exec", "--json"]',
|
|
||||||
' args: ["exec", "--json"]',
|
|
||||||
)
|
|
||||||
.replaceFirst(' # timeout: 10m', ' timeout: 10m')
|
.replaceFirst(' # timeout: 10m', ' timeout: 10m')
|
||||||
.replaceFirst(' # issueAssistant:', ' issueAssistant:')
|
.replaceFirst(' # issueAssistant:', ' issueAssistant:')
|
||||||
.replaceFirst(' # enabled: true', ' enabled: true')
|
.replaceFirst(' # enabled: true', ' enabled: true')
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,7 @@ projects:
|
||||||
test('Decode Copilot CLI JSONL assistant messages', () async {
|
test('Decode Copilot CLI JSONL assistant messages', () async {
|
||||||
// Given a responder command that emits Copilot CLI JSONL events.
|
// Given a responder command that emits Copilot CLI JSONL events.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandbox = await Directory.systemTemp.createTemp(
|
||||||
'cws-copilot-jsonl-',
|
'cws-copilot-events-',
|
||||||
);
|
);
|
||||||
final scriptFile = File(p.join(sandbox.path, 'responder.dart'));
|
final scriptFile = File(p.join(sandbox.path, 'responder.dart'));
|
||||||
await scriptFile.writeAsString('''
|
await scriptFile.writeAsString('''
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue