return error envelopes from failed claude commands (T-391)
Sixteen claude.* handlers reported ok with an `error` field buried in the payload — `clide claude.agent.set-permission-mode bogus` exited 0, so scripts could not detect failure, drifting from the D-6 exit-code contract every other subsystem honors. Missing/invalid args are now userError, missing sessions notFound, a missing orchestrator toolError, and a failed task reassign no longer reports ok:false as a success. No UI consumer read the old payloads. Table-driven test walks every failure path asserting non-zero codes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,13 @@ import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// D-6 contract (T-391): a failed command returns an ERROR envelope (non-zero
|
||||
/// CLI exit), never `ok` with an `error` field a script can't detect.
|
||||
IpcResponse _userErr(String msg, {String? hint}) =>
|
||||
IpcResponse.err(id: '', error: IpcError(code: IpcExitCode.userError, kind: IpcErrorKind.userError, message: msg, hint: hint));
|
||||
|
||||
IpcResponse _notFound(String msg) => IpcResponse.err(id: '', error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: msg));
|
||||
|
||||
class ClaudeExtension extends ClideExtension {
|
||||
@override
|
||||
String get id => 'builtin.claude';
|
||||
@@ -95,7 +102,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: show an agent session pane',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
_orchestrator?.show(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'shown'});
|
||||
},
|
||||
@@ -106,7 +113,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: hide an agent session pane',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
_orchestrator?.hide(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'hidden'});
|
||||
},
|
||||
@@ -117,7 +124,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: close (kill) an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
await _orchestrator?.close(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'closed'});
|
||||
},
|
||||
@@ -128,7 +135,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: mute broker delivery to an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
_orchestrator?.mute(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'muted'});
|
||||
},
|
||||
@@ -139,7 +146,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: unmute broker delivery to an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
_orchestrator?.unmute(id);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'unmuted'});
|
||||
},
|
||||
@@ -151,9 +158,9 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: inject a text turn into an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
final text = args.skip(1).join(' ');
|
||||
if (text.isEmpty) return IpcResponse.ok(id: '', data: const {'error': 'missing message text'});
|
||||
if (text.isEmpty) return _userErr('missing message text');
|
||||
_orchestrator?.injectMessage(id, text);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'status': 'injected'});
|
||||
},
|
||||
@@ -169,12 +176,12 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: set permission mode for an agent session',
|
||||
run: (args) async {
|
||||
final id = args.firstOrNull;
|
||||
if (id == null) return IpcResponse.ok(id: '', data: const {'error': 'missing session id'});
|
||||
if (id == null) return _userErr('missing session id');
|
||||
final mode = args.length >= 2 ? args[1] : null;
|
||||
if (mode == null) return IpcResponse.ok(id: '', data: const {'error': 'missing mode (default|acceptEdits|plan|bypassPermissions)'});
|
||||
if (mode == null) return _userErr('missing mode (default|acceptEdits|plan|bypassPermissions)');
|
||||
const valid = {'default', 'acceptEdits', 'plan', 'bypassPermissions'};
|
||||
if (!valid.contains(mode)) {
|
||||
return IpcResponse.ok(id: '', data: {'error': 'unknown mode "$mode"; use one of: ${valid.join(', ')}'});
|
||||
return _userErr('unknown mode "$mode"; use one of: ${valid.join(', ')}');
|
||||
}
|
||||
_orchestrator?.byId(id)?.session.setPermissionMode(mode);
|
||||
return IpcResponse.ok(id: '', data: {'id': id, 'mode': mode, 'status': 'sent'});
|
||||
@@ -188,7 +195,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: Cycle permission mode',
|
||||
run: (_) async {
|
||||
final managed = _orchestrator?.byId('primary');
|
||||
if (managed == null) return IpcResponse.ok(id: '', data: const {'error': 'no primary session'});
|
||||
if (managed == null) return _notFound('no primary session');
|
||||
final next = nextSafePermissionMode(managed.session.status.permissionMode ?? 'default');
|
||||
managed.session.setPermissionMode(next);
|
||||
return IpcResponse.ok(id: '', data: {'mode': next, 'status': 'sent'});
|
||||
@@ -200,11 +207,12 @@ class ClaudeExtension extends ClideExtension {
|
||||
command: 'claude.task.reassign',
|
||||
title: 'Claude: reassign a shared task to an agent',
|
||||
run: (args) async {
|
||||
if (args.length < 2) return IpcResponse.ok(id: '', data: const {'error': 'usage: <taskId> <sessionId>'});
|
||||
if (args.length < 2) return _userErr('usage: <taskId> <sessionId>');
|
||||
final taskId = args[0];
|
||||
final toId = args[1];
|
||||
final ok = _orchestrator?.broker.reassignTask(taskId, toId) ?? false;
|
||||
return IpcResponse.ok(id: '', data: {'taskId': taskId, 'toId': toId, 'ok': ok});
|
||||
if (!ok) return _notFound('could not reassign task "$taskId" to "$toId"');
|
||||
return IpcResponse.ok(id: '', data: {'taskId': taskId, 'toId': toId, 'ok': true});
|
||||
},
|
||||
),
|
||||
// T-180: full team chat pane opened as a workspace tab.
|
||||
@@ -241,7 +249,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
command: 'claude.team-chat.post',
|
||||
title: 'Claude: post a message into the team channel as the user',
|
||||
run: (args) async {
|
||||
if (args.isEmpty) return IpcResponse.ok(id: '', data: const {'error': 'usage: [@name] <text>'});
|
||||
if (args.isEmpty) return _userErr('usage: [@name] <text>');
|
||||
final raw = args.join(' ');
|
||||
String? recipient;
|
||||
String body = raw;
|
||||
@@ -269,15 +277,18 @@ class ClaudeExtension extends ClideExtension {
|
||||
run: (args) async {
|
||||
final sourceId = args.firstOrNull;
|
||||
if (sourceId == null) {
|
||||
return IpcResponse.ok(id: '', data: const {'error': 'usage: claude.agent.fork <sourceSessionId> [<cwd>]'});
|
||||
return _userErr('usage: claude.agent.fork <sourceSessionId> [<cwd>]');
|
||||
}
|
||||
final orch = _orchestrator;
|
||||
if (orch == null) {
|
||||
return IpcResponse.ok(id: '', data: const {'error': 'orchestrator unavailable'});
|
||||
return IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'orchestrator unavailable'),
|
||||
);
|
||||
}
|
||||
final source = orch.byId(sourceId);
|
||||
if (source == null) {
|
||||
return IpcResponse.ok(id: '', data: {'error': 'unknown session "$sourceId"'});
|
||||
return _notFound('unknown session "$sourceId"');
|
||||
}
|
||||
final cwd = args.length >= 2 ? args[1] : source.cwd;
|
||||
final forkId = 'fork:$sourceId-${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
Reference in New Issue
Block a user