@gobing-ai/ts-ai-runner 0.3.20 → 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,16 @@
1
- /** Identifier for one supported coding agent. */
2
- export type AgentName = 'claude' | 'codex' | 'gemini' | 'pi' | 'opencode' | 'antigravity' | 'openclaw';
1
+ import { getLogger } from '@gobing-ai/ts-infra';
2
+
3
+ /** Identifier for one supported coding agent (canonical id). */
4
+ export type AgentName =
5
+ | 'claude'
6
+ | 'codex'
7
+ | 'gemini'
8
+ | 'pi'
9
+ | 'opencode'
10
+ | 'antigravity-cli'
11
+ | 'openclaw'
12
+ | 'hermes'
13
+ | 'omp';
3
14
 
4
15
  /** Output mode for prompt invocations. */
5
16
  export type OutputMode = 'text' | 'json';
@@ -34,14 +45,26 @@ export interface PromptOptions {
34
45
  peers?: Array<{ id: string; type: string; purpose?: string }>;
35
46
  }
36
47
 
48
+ /** Deprecation metadata attached to a retired or superseded agent shim. */
49
+ export interface AgentDeprecation {
50
+ /** Release date (ISO) when the id was marked deprecated. */
51
+ readonly since: string;
52
+ /** Canonical replacement, when one exists. */
53
+ readonly replacedBy?: AgentName;
54
+ }
55
+
37
56
  /** Pure command builder for one coding-agent CLI. */
38
57
  export interface AgentShim {
39
- /** Stable agent identifier. */
58
+ /** Stable canonical agent identifier. */
40
59
  readonly name: AgentName;
41
60
  /** Executable command name. */
42
61
  readonly command: string;
43
62
  /** Capability tier: 1 = direct CLI, 2 = gateway/TUI constrained. */
44
63
  readonly tier: 1 | 2;
64
+ /** Non-canonical ids that resolve to this shim (aliases map to this canonical id). */
65
+ readonly aliases?: readonly string[];
66
+ /** Deprecation marker; resolving a deprecated id warns and reports canonical status. */
67
+ readonly deprecated?: AgentDeprecation;
45
68
  /** Build a help-display command. */
46
69
  getHelpCommand(): ShimCommand;
47
70
  /** Build a version-detection command. */
@@ -90,6 +113,7 @@ const geminiShim: AgentShim = {
90
113
  name: 'gemini',
91
114
  command: 'gemini',
92
115
  tier: 1,
116
+ deprecated: { since: '2026-06-20', replacedBy: 'antigravity-cli' },
93
117
  getHelpCommand: () => ({ command: 'gemini', args: ['--help'] }),
94
118
  getVersionCommand: () => ({ command: 'gemini', args: ['--version'] }),
95
119
  getPromptCommand: (options) => {
@@ -136,13 +160,24 @@ const opencodeShim: AgentShim = {
136
160
  getAuthCommand: () => ({ command: 'opencode', args: ['providers'] }),
137
161
  };
138
162
 
139
- const antigravityShim: AgentShim = {
140
- name: 'antigravity',
163
+ /**
164
+ * Antigravity CLI (`agy`) — the scriptable/headless successor to Gemini CLI
165
+ * (Antigravity 2.0 split, 2026-06). Tier-1: `-p`/`--print` one-shot, `--model`,
166
+ * `agy models`, `--continue` resumes the most recent session.
167
+ */
168
+ const antigravityCliShim: AgentShim = {
169
+ name: 'antigravity-cli',
141
170
  command: 'agy',
142
- tier: 2,
171
+ tier: 1,
172
+ aliases: ['antigravity'],
143
173
  getHelpCommand: () => ({ command: 'agy', args: ['--help'] }),
144
174
  getVersionCommand: () => ({ command: 'agy', args: ['--version'] }),
145
- getPromptCommand: (options) => ({ command: 'agy', args: ['chat', options.input ?? ''] }),
175
+ getPromptCommand: (options) => {
176
+ const args = ['-p', options.input ?? ''];
177
+ if (options.continue === true) args.push('--continue');
178
+ if (options.model !== undefined) args.push('--model', options.model);
179
+ return { command: 'agy', args };
180
+ },
146
181
  getAuthCommand: () => null,
147
182
  };
148
183
 
@@ -156,19 +191,70 @@ const openclawShim: AgentShim = {
156
191
  getAuthCommand: () => ({ command: 'openclaw', args: ['health'] }),
157
192
  };
158
193
 
159
- /** All bundled agent shims keyed by agent name. */
194
+ /**
195
+ * Hermes Agent (NousResearch) — OpenClaw-compatible coding agent. One-shot via
196
+ * `hermes chat -q <input>`; model/provider overrides; `hermes doctor` health probe.
197
+ */
198
+ const hermesShim: AgentShim = {
199
+ name: 'hermes',
200
+ command: 'hermes',
201
+ tier: 1,
202
+ getHelpCommand: () => ({ command: 'hermes', args: ['chat', '--help'] }),
203
+ getVersionCommand: () => ({ command: 'hermes', args: ['--version'] }),
204
+ getPromptCommand: (options) => {
205
+ const args = ['chat', '-q', options.input ?? ''];
206
+ if (options.continue === true) args.push('--continue');
207
+ if (options.model !== undefined) args.push('-m', options.model);
208
+ return { command: 'hermes', args };
209
+ },
210
+ getAuthCommand: () => ({ command: 'hermes', args: ['doctor'] }),
211
+ };
212
+
213
+ /**
214
+ * omp (oh-my-pi) — a Pi fork. argv is Pi-compatible for the one-shot surface
215
+ * (`--no-session`, `-p`, `-c`, `--model`, `--mode`); speaks Pi's `/skill:` slash dialect.
216
+ */
217
+ const ompShim: AgentShim = {
218
+ name: 'omp',
219
+ command: 'omp',
220
+ tier: 1,
221
+ getHelpCommand: () => ({ command: 'omp', args: ['--help'] }),
222
+ getVersionCommand: () => ({ command: 'omp', args: ['--version'] }),
223
+ getPromptCommand: (options) => {
224
+ const args: string[] = [];
225
+ if (options.continue !== true) args.push('--no-session');
226
+ args.push('-p', options.input ?? '');
227
+ if (options.continue === true) args.push('-c');
228
+ if (options.model !== undefined) args.push('--model', options.model);
229
+ args.push('--mode', options.mode ?? 'text');
230
+ return { command: 'omp', args };
231
+ },
232
+ getAuthCommand: () => ({ command: 'omp', args: ['--list-models'] }),
233
+ };
234
+
235
+ /** All bundled agent shims keyed by canonical agent name. */
160
236
  export const AGENT_SHIMS: Readonly<Record<AgentName, AgentShim>> = {
161
237
  claude: claudeShim,
162
238
  codex: codexShim,
163
239
  gemini: geminiShim,
164
240
  pi: piShim,
165
241
  opencode: opencodeShim,
166
- antigravity: antigravityShim,
242
+ 'antigravity-cli': antigravityCliShim,
167
243
  openclaw: openclawShim,
244
+ hermes: hermesShim,
245
+ omp: ompShim,
168
246
  };
169
247
 
170
- /** Tier-1 auto-selection priority. */
171
- export const TIER1_PRIORITY: readonly AgentName[] = ['pi', 'codex', 'gemini', 'claude', 'opencode'];
248
+ /** Tier-1 auto-selection priority. Deprecated ids are excluded. */
249
+ export const TIER1_PRIORITY: readonly AgentName[] = [
250
+ 'pi',
251
+ 'omp',
252
+ 'codex',
253
+ 'antigravity-cli',
254
+ 'claude',
255
+ 'hermes',
256
+ 'opencode',
257
+ ];
172
258
 
173
259
  /** Display order for doctor and list commands. */
174
260
  export const DISPLAY_ORDER: readonly AgentName[] = [
@@ -176,20 +262,73 @@ export const DISPLAY_ORDER: readonly AgentName[] = [
176
262
  'codex',
177
263
  'gemini',
178
264
  'pi',
265
+ 'omp',
179
266
  'opencode',
180
- 'antigravity',
267
+ 'antigravity-cli',
181
268
  'openclaw',
269
+ 'hermes',
182
270
  ];
183
271
 
184
272
  /** Set of gateway/TUI-constrained agents. */
185
- export const TIER2_AGENTS: ReadonlySet<AgentName> = new Set(['antigravity', 'openclaw']);
273
+ export const TIER2_AGENTS: ReadonlySet<AgentName> = new Set(['openclaw']);
186
274
 
187
- /** Return true when a value is a supported agent identifier. */
188
- export function isAgentName(value: string): value is AgentName {
189
- return Object.hasOwn(AGENT_SHIMS, value);
275
+ /** Logger for alias/deprecation resolution diagnostics. */
276
+ const logger = getLogger('ai-runner.shims');
277
+
278
+ /** Alias → canonical id, derived once from `AGENT_SHIMS[*].aliases`. */
279
+ const ALIAS_TO_CANONICAL: Readonly<Record<string, AgentName>> = Object.fromEntries(
280
+ Object.values(AGENT_SHIMS).flatMap((shim) => (shim.aliases ?? []).map((alias) => [alias, shim.name] as const)),
281
+ );
282
+
283
+ /**
284
+ * Resolve a canonical or alias id to its canonical `AgentName`.
285
+ *
286
+ * - Canonical ids resolve to themselves.
287
+ * - Aliases resolve to their canonical id (e.g. `'antigravity' → 'antigravity-cli'`).
288
+ * - Unknown ids resolve to `undefined`.
289
+ *
290
+ * Resolving a deprecated or aliased id emits exactly one `warn` through the
291
+ * logger seam and never throws.
292
+ */
293
+ export function resolveAgentName(input: string): AgentName | undefined {
294
+ if (isCanonicalName(input)) return input;
295
+ const canonical = ALIAS_TO_CANONICAL[input];
296
+ if (canonical !== undefined) {
297
+ warnDeprecatedOrAlias(input, canonical);
298
+ return canonical;
299
+ }
300
+ return undefined;
301
+ }
302
+
303
+ /** Return true when a value is a known canonical id or alias. Does not narrow;
304
+ * use `resolveAgentName()` to obtain the canonical `AgentName`. */
305
+ export function isAgentName(value: string): boolean {
306
+ return isCanonicalName(value) || value in ALIAS_TO_CANONICAL;
190
307
  }
191
308
 
192
- /** Look up a bundled agent shim. */
309
+ /** Look up a bundled agent shim, resolving aliases to the canonical shim. */
193
310
  export function getAgentShim(agent: AgentName): AgentShim {
194
- return AGENT_SHIMS[agent];
311
+ const canonical = resolveAgentName(agent);
312
+ if (canonical === undefined) {
313
+ throw new Error(`Unsupported agent: ${agent}`);
314
+ }
315
+ return AGENT_SHIMS[canonical];
316
+ }
317
+
318
+ function isCanonicalName(value: string): value is AgentName {
319
+ return Object.hasOwn(AGENT_SHIMS, value);
320
+ }
321
+
322
+ function warnDeprecatedOrAlias(input: string, canonical: AgentName): void {
323
+ const shim = AGENT_SHIMS[canonical];
324
+ if (shim.deprecated !== undefined) {
325
+ const replacement = shim.deprecated.replacedBy ?? canonical;
326
+ logger.warn(`agent '${input}' is deprecated (since ${shim.deprecated.since}); use '${replacement}'`, {
327
+ input,
328
+ canonical,
329
+ replacedBy: replacement,
330
+ });
331
+ } else {
332
+ logger.warn(`agent '${input}' is an alias; resolving to canonical '${canonical}'`, { input, canonical });
333
+ }
195
334
  }
package/src/ai-runner.ts CHANGED
@@ -113,7 +113,9 @@ export class AiRunner {
113
113
 
114
114
  /** Build an agent prompt command without executing it. */
115
115
  buildPromptCommand(agent: AgentName, promptOptions: PromptOptions, options: AgentRunOptions = {}): ShimCommand {
116
- return getAgentShim(agent).getPromptCommand(this.withIdentityPreamble(agent, promptOptions, options));
116
+ return buildAgentCommand(agent, promptOptions, {
117
+ workspace: options.cwd ?? this.defaultCwd ?? getProcessCwd(),
118
+ });
117
119
  }
118
120
 
119
121
  /** Run an agent authentication command, or return null when unsupported. */
@@ -164,26 +166,6 @@ export class AiRunner {
164
166
  durationMs: result.durationMs,
165
167
  };
166
168
  }
167
-
168
- private withIdentityPreamble(
169
- agent: AgentName,
170
- promptOptions: PromptOptions,
171
- options: AgentRunOptions,
172
- ): PromptOptions {
173
- if (!hasIdentityOptions(promptOptions)) return promptOptions;
174
- const workspace = options.cwd ?? this.defaultCwd ?? getProcessCwd();
175
- const preamble = buildIdentityPreamble({
176
- agentId: agent,
177
- agentType: agent,
178
- workspace,
179
- purpose: promptOptions.purpose,
180
- systemPrompt: promptOptions.systemPrompt,
181
- taskId: promptOptions.taskId,
182
- peers: promptOptions.peers,
183
- });
184
- const input = promptOptions.input === undefined ? preamble : `${preamble}\n${promptOptions.input}`;
185
- return { ...promptOptions, input };
186
- }
187
169
  }
188
170
 
189
171
  function hasIdentityOptions(options: PromptOptions): boolean {
@@ -194,3 +176,36 @@ function hasIdentityOptions(options: PromptOptions): boolean {
194
176
  (options.peers !== undefined && options.peers.length > 0)
195
177
  );
196
178
  }
179
+
180
+ /**
181
+ * Shared command-build seam: identity preamble + shim dispatch. Both the
182
+ * one-shot path (`AiRunner.buildPromptCommand`) and the team-mode path
183
+ * (`TeamOrchestrator.startAgent`) resolve argv through this single function
184
+ * so equivalent `PromptOptions` produce equivalent argv (R5 parity).
185
+ */
186
+ export function buildAgentCommand(
187
+ agent: AgentName,
188
+ promptOptions: PromptOptions,
189
+ context: { workspace: string },
190
+ ): ShimCommand {
191
+ return getAgentShim(agent).getPromptCommand(applyIdentityPreamble(agent, promptOptions, context));
192
+ }
193
+
194
+ function applyIdentityPreamble(
195
+ agent: AgentName,
196
+ promptOptions: PromptOptions,
197
+ context: { workspace: string },
198
+ ): PromptOptions {
199
+ if (!hasIdentityOptions(promptOptions)) return promptOptions;
200
+ const preamble = buildIdentityPreamble({
201
+ agentId: agent,
202
+ agentType: agent,
203
+ workspace: context.workspace,
204
+ purpose: promptOptions.purpose,
205
+ systemPrompt: promptOptions.systemPrompt,
206
+ taskId: promptOptions.taskId,
207
+ peers: promptOptions.peers,
208
+ });
209
+ const input = promptOptions.input === undefined ? preamble : `${preamble}\n${promptOptions.input}`;
210
+ return { ...promptOptions, input };
211
+ }
@@ -1,7 +1,7 @@
1
1
  import { getLogger, type Logger } from '@gobing-ai/ts-infra';
2
- import { getProcessEnv, joinPath, NodeFileSystem } from '@gobing-ai/ts-runtime';
2
+ import { createNodeFileSystem, type FileSystem, getProcessEnv, joinPath } from '@gobing-ai/ts-runtime';
3
3
  import { AgentDetector, type DetectedAgent } from './agent-detector';
4
- import { type AgentName, DISPLAY_ORDER, isAgentName, TIER2_AGENTS } from './agents/shims';
4
+ import { type AgentName, DISPLAY_ORDER, resolveAgentName, TIER2_AGENTS } from './agents/shims';
5
5
  import { type AgentRunResult, AiRunner } from './ai-runner';
6
6
 
7
7
  /** Health-check result for one coding agent. */
@@ -20,6 +20,10 @@ export interface DoctorResult {
20
20
  tier: 1 | 2;
21
21
  /** Agent-specific channels or models when available. */
22
22
  channels: string[];
23
+ /** True when the resolved canonical id is marked deprecated. */
24
+ deprecated?: boolean;
25
+ /** Canonical replacement when the resolved id is deprecated, if any. */
26
+ replacedBy?: AgentName;
23
27
  /** Probe error when unavailable. */
24
28
  error: string | null;
25
29
  }
@@ -36,6 +40,8 @@ export interface DoctorRunnerOptions {
36
40
  env?: Record<string, string | undefined>;
37
41
  /** Logger for health-check diagnostics. Defaults to `getLogger('doctor')`. */
38
42
  logger?: Logger;
43
+ /** Filesystem for auth-file checks. Defaults to `createNodeFileSystem()`; inject to test without disk. */
44
+ fileSystem?: FileSystem;
39
45
  }
40
46
 
41
47
  const DEFAULT_TIMEOUT_MS = 5_000;
@@ -61,6 +67,14 @@ const AUTH_PATTERNS: Partial<Record<AgentName, { positive: RegExp; negative: Reg
61
67
  positive: /\S/,
62
68
  negative: /not[\s_-]*authenticated|not[\s_-]*logged[\s_-]*in|unauthenticated|no[\s_-]+providers?/i,
63
69
  },
70
+ omp: {
71
+ positive: /\S/,
72
+ negative: /not[\s_-]*authenticated|not[\s_-]*logged[\s_-]*in|unauthenticated|no[\s_-]+providers?/i,
73
+ },
74
+ hermes: {
75
+ positive: /(^|[^a-z])ok([^a-z]|$)|healthy|configured|ready/i,
76
+ negative: /not[\s_-]*(configured|healthy|ok)|unhealthy|missing[\s_-]+dependenc|error|failed/i,
77
+ },
64
78
  };
65
79
 
66
80
  /** True when a value is a defined, non-blank string. */
@@ -74,7 +88,7 @@ export class DoctorRunner {
74
88
  private readonly runner: AiRunner;
75
89
  private readonly timeout: number;
76
90
  private readonly env: Record<string, string | undefined>;
77
- private readonly fs = new NodeFileSystem();
91
+ private readonly fs: FileSystem;
78
92
  private readonly logger: Logger;
79
93
 
80
94
  constructor(options: DoctorRunnerOptions = {}) {
@@ -82,6 +96,7 @@ export class DoctorRunner {
82
96
  this.detector = options.agentDetector ?? new AgentDetector({ runner: this.runner });
83
97
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
84
98
  this.env = options.env ?? getProcessEnv();
99
+ this.fs = options.fileSystem ?? createNodeFileSystem();
85
100
  this.logger = options.logger ?? getLogger('doctor');
86
101
  }
87
102
 
@@ -110,10 +125,10 @@ export class DoctorRunner {
110
125
  }
111
126
 
112
127
  private async buildResult(detected: DetectedAgent): Promise<DoctorResult> {
113
- const tier = TIER2_AGENTS.has(detected.name as AgentName) ? 2 : 1;
128
+ const canonical = resolveAgentName(detected.name) ?? null;
129
+ const tier = canonical !== null && TIER2_AGENTS.has(canonical) ? 2 : 1;
114
130
  this.logger.debug('checking agent', { agent: detected.name, installed: detected.installed, tier });
115
- const authenticated =
116
- detected.installed && isAgentName(detected.name) ? await this.checkAuth(detected.name) : false;
131
+ const authenticated = detected.installed && canonical !== null ? await this.checkAuth(canonical) : false;
117
132
  return {
118
133
  agent: detected.name,
119
134
  installed: detected.installed,
@@ -122,6 +137,8 @@ export class DoctorRunner {
122
137
  usable: detected.installed && detected.version !== null && authenticated,
123
138
  tier,
124
139
  channels: detected.channels,
140
+ deprecated: detected.deprecated,
141
+ replacedBy: detected.replacedBy,
125
142
  error: detected.error,
126
143
  };
127
144
  }
@@ -130,9 +147,12 @@ export class DoctorRunner {
130
147
  const home = this.env.HOME || this.env.USERPROFILE || '';
131
148
  if (agent === 'gemini') return this.geminiSettingsContainCredentials(home);
132
149
  if (agent === 'codex') return this.checkCodexAuth(home);
133
- // pi reads provider keys from the environment; require a non-empty value
134
- // rather than mere presence (an empty export is not a usable credential).
135
- if (agent === 'pi' && (isNonEmpty(this.env.GOOGLE_API_KEY) || isNonEmpty(this.env.ANTHROPIC_API_KEY)))
150
+ // pi and omp read provider keys from the environment; require a non-empty
151
+ // value rather than mere presence (an empty export is not a usable credential).
152
+ if (
153
+ (agent === 'pi' || agent === 'omp') &&
154
+ (isNonEmpty(this.env.GOOGLE_API_KEY) || isNonEmpty(this.env.ANTHROPIC_API_KEY))
155
+ )
136
156
  return true;
137
157
  return (await this.probeAuthOutput(agent)) === true;
138
158
  }
@@ -35,6 +35,7 @@ export function translateSlashCommand(agent: AgentName, input: string): string {
35
35
  case 'codex':
36
36
  return `$${plugin}-${command}${suffix}`;
37
37
  case 'pi':
38
+ case 'omp':
38
39
  return `/skill:${plugin}-${command}${suffix}`;
39
40
  default:
40
41
  return `/${plugin}-${command}${suffix}`;
@@ -2,9 +2,9 @@ import type { InboxMessageDao } from '@gobing-ai/ts-db/inbox';
2
2
  import { EventBus } from '@gobing-ai/ts-infra';
3
3
  import type { AgentSpec } from './agent-spec';
4
4
  import { loadAgentSpecs } from './agent-spec';
5
- import { type AgentName, getAgentShim, isAgentName } from './agents/shims';
5
+ import { type AgentName, resolveAgentName } from './agents/shims';
6
+ import { buildAgentCommand } from './ai-runner';
6
7
  import type { AgentEvents } from './events';
7
- import { buildIdentityPreamble } from './identity';
8
8
  import { formatMessage } from './messages';
9
9
  import { TeamAgentProcess } from './team-agent-process';
10
10
 
@@ -35,38 +35,33 @@ export class TeamOrchestrator {
35
35
  this.events = options.events ?? new EventBus<AgentEvents>();
36
36
  }
37
37
 
38
- loadSpecs(): AgentSpec[] {
39
- this.specs = loadAgentSpecs(this.configDir);
38
+ async loadSpecs(): Promise<AgentSpec[]> {
39
+ this.specs = await loadAgentSpecs(this.configDir);
40
40
  return [...this.specs];
41
41
  }
42
42
 
43
- getSpec(id: string): AgentSpec | undefined {
44
- if (this.specs.length === 0) this.loadSpecs();
43
+ async getSpec(id: string): Promise<AgentSpec | undefined> {
44
+ if (this.specs.length === 0) await this.loadSpecs();
45
45
  return this.specs.find((spec) => spec.id === id);
46
46
  }
47
47
 
48
48
  async startAgent(id: string): Promise<TeamAgentProcess> {
49
- const spec = this.requireSpec(id);
49
+ const spec = await this.requireSpec(id);
50
50
  const agentType = this.requireAgentName(spec.type);
51
- const peers = this.getPeerSpecs(spec.workspace, spec.id).map((peer) => ({
51
+ const peers = (await this.getPeerSpecs(spec.workspace, spec.id)).map((peer) => ({
52
52
  id: peer.id,
53
53
  type: peer.type,
54
54
  purpose: peer.purpose,
55
55
  }));
56
- const identityPreamble = buildIdentityPreamble({
57
- agentId: spec.id,
58
- agentType: spec.type,
59
- workspace: spec.workspace,
60
- purpose: spec.purpose,
61
- systemPrompt: typeof spec.config.systemPrompt === 'string' ? spec.config.systemPrompt : undefined,
62
- peers,
63
- });
64
- const command = getAgentShim(agentType).getPromptCommand({
65
- input: identityPreamble,
66
- purpose: spec.purpose,
67
- systemPrompt: typeof spec.config.systemPrompt === 'string' ? spec.config.systemPrompt : undefined,
68
- peers,
69
- });
56
+ const command = buildAgentCommand(
57
+ agentType,
58
+ {
59
+ purpose: spec.purpose,
60
+ systemPrompt: typeof spec.config.systemPrompt === 'string' ? spec.config.systemPrompt : undefined,
61
+ peers,
62
+ },
63
+ { workspace: spec.workspace },
64
+ );
70
65
  const process = this.processFactory({
71
66
  spec,
72
67
  command: [command.command, ...command.args],
@@ -108,14 +103,14 @@ export class TeamOrchestrator {
108
103
  return new Map(this.running);
109
104
  }
110
105
 
111
- getAgentStatus(id: string): 'running' | 'stopped' | 'errored' | 'unknown' {
106
+ async getAgentStatus(id: string): Promise<'running' | 'stopped' | 'errored' | 'unknown'> {
112
107
  const process = this.running.get(id);
113
- if (process === undefined) return this.getSpec(id) === undefined ? 'unknown' : 'stopped';
108
+ if (process === undefined) return (await this.getSpec(id)) === undefined ? 'unknown' : 'stopped';
114
109
  return process.getStatus();
115
110
  }
116
111
 
117
- getPeerSpecs(workspace: string, excludeId?: string): AgentSpec[] {
118
- if (this.specs.length === 0) this.loadSpecs();
112
+ async getPeerSpecs(workspace: string, excludeId?: string): Promise<AgentSpec[]> {
113
+ if (this.specs.length === 0) await this.loadSpecs();
119
114
  return this.specs.filter((spec) => spec.workspace === workspace && spec.id !== excludeId);
120
115
  }
121
116
 
@@ -128,15 +123,16 @@ export class TeamOrchestrator {
128
123
  return () => this.events.off(event, listener);
129
124
  }
130
125
 
131
- private requireSpec(id: string): AgentSpec {
132
- const spec = this.getSpec(id);
126
+ private async requireSpec(id: string): Promise<AgentSpec> {
127
+ const spec = await this.getSpec(id);
133
128
  if (spec === undefined) throw new Error(`Agent spec not found: ${id}`);
134
129
  return spec;
135
130
  }
136
131
 
137
132
  private requireAgentName(type: string): AgentName {
138
- if (!isAgentName(type)) throw new Error(`Unsupported agent type: ${type}`);
139
- return type;
133
+ const canonical = resolveAgentName(type);
134
+ if (canonical === undefined) throw new Error(`Unsupported agent type: ${type}`);
135
+ return canonical;
140
136
  }
141
137
 
142
138
  private async injectPendingMessages(process: TeamAgentProcess): Promise<boolean> {