@gobing-ai/ts-ai-runner 0.3.1 → 0.3.2

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.
Files changed (46) hide show
  1. package/README.md +336 -32
  2. package/dist/agent-detector.d.ts +1 -0
  3. package/dist/agent-detector.d.ts.map +1 -1
  4. package/dist/agent-detector.js +13 -5
  5. package/dist/agent-spec.d.ts +6 -0
  6. package/dist/agent-spec.d.ts.map +1 -1
  7. package/dist/agent-spec.js +12 -8
  8. package/dist/ai-runner.d.ts +18 -2
  9. package/dist/ai-runner.d.ts.map +1 -1
  10. package/dist/ai-runner.js +52 -6
  11. package/dist/doctor-runner.d.ts +7 -0
  12. package/dist/doctor-runner.d.ts.map +1 -1
  13. package/dist/doctor-runner.js +69 -15
  14. package/dist/events.d.ts +38 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +0 -0
  17. package/dist/identity.d.ts +3 -0
  18. package/dist/identity.d.ts.map +1 -1
  19. package/dist/identity.js +2 -0
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/messages.d.ts +4 -0
  24. package/dist/messages.d.ts.map +1 -0
  25. package/dist/messages.js +4 -0
  26. package/dist/team-agent-process.d.ts +12 -4
  27. package/dist/team-agent-process.d.ts.map +1 -1
  28. package/dist/team-agent-process.js +31 -19
  29. package/dist/team-orchestrator.d.ts +13 -8
  30. package/dist/team-orchestrator.d.ts.map +1 -1
  31. package/dist/team-orchestrator.js +32 -25
  32. package/package.json +4 -4
  33. package/src/agent-detector.ts +14 -5
  34. package/src/agent-spec.ts +20 -8
  35. package/src/ai-runner.ts +75 -13
  36. package/src/doctor-runner.ts +77 -16
  37. package/src/events.ts +25 -0
  38. package/src/identity.ts +3 -0
  39. package/src/index.ts +2 -1
  40. package/src/messages.ts +6 -0
  41. package/src/team-agent-process.ts +36 -21
  42. package/src/team-orchestrator.ts +36 -25
  43. package/dist/message-service.d.ts +0 -13
  44. package/dist/message-service.d.ts.map +0 -1
  45. package/dist/message-service.js +0 -27
  46. package/src/message-service.ts +0 -33
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @gobing-ai/ts-ai-runner
2
2
 
3
- Coding-agent command shims, installation detection, doctor checks, slash-command translation, and prompt execution for downstream CLIs.
3
+ Coding-agent command shims, installation detection, doctor checks, slash-command translation, identity preamble construction, and team-mode orchestration for downstream CLIs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @gobing-ai/ts-ai-runner
9
+ ```
4
10
 
5
11
  ## What It Provides
6
12
 
@@ -8,26 +14,175 @@ Coding-agent command shims, installation detection, doctor checks, slash-command
8
14
 
9
15
  | Export | Purpose |
10
16
  |--------|---------|
11
- | `AiRunner` | Runs help, version, auth, and prompt commands through a pluggable process executor |
17
+ | `AiRunner` | Runs help, version, auth, prompt, and slash commands through a pluggable process executor; can also build a prompt command without executing it. Emits typed events via optional `EventBus<AgentEvents>`. |
12
18
  | `AgentDetector` | Probes supported agent CLIs and parses version output |
13
19
  | `DoctorRunner` | Combines installation and authentication checks into a usability report |
14
20
  | `getAgentShim()` | Returns the pure command builder for one supported agent |
15
21
  | `translateSlashCommand()` | Converts Claude-style `/plugin:command` inputs to each agent's dialect |
16
- | `buildIdentityPreamble()` | Builds team-mode identity and communication context for prompts |
17
- | `loadAgentSpecs()` / `saveAgentSpec()` | Persist agent definitions as YAML-compatible config |
22
+ | `isClaudeStyleSlashCommand()` | Tests whether input matches the `/plugin:command` pattern |
23
+ | `buildIdentityPreamble()` / `getGitContext()` | Builds team-mode identity, communication context, and git metadata for prompts |
24
+ | `loadAgentSpecs()` / `saveAgentSpec()` / `deleteAgentSpec()` | Persist agent definitions as YAML-compatible config |
25
+ | `validateAgentId()` | Enforces agent ID format rules |
18
26
  | `MessageService` | Thin service wrapper around `@gobing-ai/ts-db/inbox` |
19
27
  | `TeamAgentProcess` | Manages a long-running agent subprocess with pipe-mode stdin/stdout |
20
- | `TeamOrchestrator` | Loads specs, starts/stops agents, and routes durable/live messages |
21
-
22
- Supported agent identifiers are `claude`, `codex`, `gemini`, `pi`, `opencode`, `antigravity`, and `openclaw`.
28
+ | `TeamOrchestrator` | Loads specs, starts/stops agents, routes durable/live messages, and emits lifecycle events |
29
+ | `AgentEvents` / `AiRunnerProcessEvents` | Typed event maps for agent and process-level observability |
30
+ | `AGENT_SHIMS` / `TIER1_PRIORITY` / `TIER2_AGENTS` / `DISPLAY_ORDER` | Agent registry constants |
31
+ | `isAgentName()` | Type guard for supported agent identifiers |
32
+
33
+ Supported agent identifiers: `claude`, `codex`, `gemini`, `pi`, `opencode` (tier 1), `antigravity`, `openclaw` (tier 2).
34
+
35
+ ## Architecture
36
+
37
+ ```mermaid
38
+ graph TB
39
+ subgraph Shared ["Shared components"]
40
+ AgentShim["AgentShim<br/>(pure command builder per agent)"]
41
+ Identity["buildIdentityPreamble()<br/>(team-mode identity context)"]
42
+ EventBus["EventBus&lt;AgentEvents&gt;<br/>(opt-in observability)"]
43
+ ProcessExecutor["ProcessExecutor<br/>(injectable, ts-runtime)"]
44
+ end
45
+
46
+ subgraph OneShot ["One-shot mode"]
47
+ AiRunner["AiRunner<br/>runPromptCommand / runSlashCommand<br/>buildPromptCommand / runHelpCommand<br/>runVersionCommand / runAuthCommand"]
48
+ AgentDetector["AgentDetector<br/>(probes CLIs via AiRunner)"]
49
+ DoctorRunner["DoctorRunner<br/>(install + auth health checks)"]
50
+
51
+ AiRunner -->|"resolves command via"| AgentShim
52
+ AiRunner -->|"delegates execution to"| ProcessExecutor
53
+ AgentDetector -->|"probes versions via"| AiRunner
54
+ DoctorRunner -->|"probes install + auth via"| AiRunner
55
+ AiRunner -.->|"emits events"| EventBus
56
+ AiRunner -.->|"enriches prompt"| Identity
57
+ end
58
+
59
+ subgraph TeamMode ["Team mode (building blocks)"]
60
+ TeamOrchestrator["TeamOrchestrator<br/>loadSpecs / startAgent / stopAgent<br/>sendMessage / getAgentStatus / stopAll"]
61
+ AgentSpec["AgentSpec<br/>(YAML config)<br/>load / save / delete"]
62
+ TeamAgentProcess["TeamAgentProcess<br/>(pipe-mode subprocess)<br/>start / stop / send / subscribe"]
63
+ MessageService["MessageService<br/>(InboxMessageDao wrapper)<br/>enqueue / drain / deliver / fail"]
64
+
65
+ TeamOrchestrator -->|"loads"| AgentSpec
66
+ TeamOrchestrator -->|"creates + manages"| TeamAgentProcess
67
+ TeamOrchestrator -->|"routes through"| MessageService
68
+ TeamOrchestrator -->|"resolves command via"| AgentShim
69
+ TeamOrchestrator -.->|"emits events"| EventBus
70
+ TeamOrchestrator -.->|"builds preamble"| Identity
71
+ TeamAgentProcess -->|"spawns via"| ProcessExecutor
72
+ end
73
+ ```
23
74
 
24
- ## Installation
75
+ ### One-shot prompt flow
76
+ ```mermaid
77
+ sequenceDiagram
78
+ participant Caller
79
+ participant AiRunner
80
+ participant AgentShim
81
+ participant Identity as buildIdentityPreamble
82
+ participant Slash as translateSlashCommand
83
+ participant Executor as ProcessExecutor
84
+ participant Agent as Coding Agent (CLI)
85
+ participant EventBus
86
+
87
+ Caller->>AiRunner: runPromptCommand("codex", options)
88
+ AiRunner->>AiRunner: hasIdentityOptions(options)?
89
+ alt has team-mode fields
90
+ AiRunner->>Identity: buildIdentityPreamble({agentId, peers, ...})
91
+ Identity-->>AiRunner: preamble text
92
+ AiRunner->>AiRunner: prepend preamble to input
93
+ end
94
+ AiRunner->>AgentShim: getPromptCommand(enrichedOptions)
95
+ AgentShim-->>AiRunner: { command, args }
96
+ AiRunner->>EventBus: emit("agent.invoke.start")
97
+ AiRunner->>Executor: run({ command, args, cwd, timeout })
98
+ Executor->>Agent: spawn CLI subprocess
99
+ Agent-->>Executor: stdout / stderr / exit
100
+ Executor-->>AiRunner: ProcessResult { exitCode, stdout, stderr, durationMs }
101
+ AiRunner->>EventBus: emit("agent.invoke.exit")
102
+ AiRunner-->>Caller: AgentRunResult
103
+ ```
25
104
 
26
- ```bash
27
- bun add @gobing-ai/ts-ai-runner
105
+ `runSlashCommand()` follows the same path with an extra step: it calls `translateSlashCommand()` to convert the slash input to the target agent's dialect before delegating to `runPromptCommand()`.
106
+
107
+ ### Team mode lifecycle
108
+
109
+ ```mermaid
110
+ sequenceDiagram
111
+ participant Host as Host App
112
+ participant Orch as TeamOrchestrator
113
+ participant Spec as AgentSpec (filesystem)
114
+ participant Shim as AgentShim
115
+ participant Identity as buildIdentityPreamble
116
+ participant Proc as TeamAgentProcess
117
+ participant Msg as MessageService
118
+ participant DB as InboxMessageDao
119
+ participant Agent as Coding Agent (CLI)
120
+ participant EventBus
121
+
122
+ Note over Host,EventBus: Starting an agent
123
+ Host->>Orch: startAgent("coder")
124
+ Orch->>Spec: loadAgentSpecs(configDir)
125
+ Spec-->>Orch: AgentSpec[]
126
+ Orch->>Orch: requireSpec("coder") → spec
127
+ Orch->>Orch: getPeerSpecs(workspace, "coder") → peers
128
+ Orch->>Identity: buildIdentityPreamble({agentId, peers, ...})
129
+ Identity-->>Orch: preamble
130
+ Orch->>Shim: getPromptCommand({input: preamble, ...})
131
+ Shim-->>Orch: { command, args }
132
+ Orch->>Proc: new TeamAgentProcess({spec, command})
133
+ Orch->>Proc: start()
134
+ Proc->>Agent: spawn pipe-mode subprocess
135
+ Agent-->>Proc: stdout/stderr streams
136
+ Orch->>Msg: drain("coder")
137
+ Msg->>DB: drain(toId)
138
+ DB-->>Msg: pending messages
139
+ Msg-->>Orch: messages[]
140
+ alt pending messages exist
141
+ loop for each message
142
+ Orch->>Proc: send(formattedMessage)
143
+ Proc->>Agent: write to stdin
144
+ Orch->>Msg: deliver(msg.id)
145
+ end
146
+ end
147
+ Orch->>EventBus: emit("agent.started")
148
+ Orch-->>Host: TeamAgentProcess
149
+
150
+ Note over Host,EventBus: Sending a message (durable + live)
151
+ Host->>Orch: sendMessage(null, "coder", "Implement task 0005")
152
+ Orch->>Msg: enqueue(null, "coder", body)
153
+ Msg->>DB: enqueue(from, to, body)
154
+ DB-->>Msg: messageId
155
+ Msg-->>Orch: messageId
156
+ alt agent is running
157
+ Orch->>Msg: drain("coder")
158
+ Msg->>DB: drain(toId)
159
+ DB-->>Msg: pending messages
160
+ loop for each message
161
+ Orch->>Proc: send(formattedMessage)
162
+ Proc->>Agent: write to stdin
163
+ Orch->>Msg: deliver(msg.id)
164
+ end
165
+ end
166
+ Orch->>EventBus: emit("agent.message.sent")
167
+ Orch-->>Host: messageId
168
+
169
+ Note over Host,EventBus: Stopping an agent
170
+ Host->>Orch: stopAgent("coder")
171
+ Orch->>Proc: stop()
172
+ Proc->>Agent: SIGTERM / kill
173
+ Orch->>Orch: running.delete("coder")
174
+ Orch->>EventBus: emit("agent.stopped")
175
+ Orch-->>Host: void
28
176
  ```
29
177
 
30
- The package depends on `@gobing-ai/ts-runtime` for process execution and `@gobing-ai/ts-db` for team-mode inbox types. The target agent CLIs are not bundled; install them separately in the host environment.
178
+ Key design decisions:
179
+
180
+ - **Shims are pure**: `AgentShim` produces `{ command, args }` without touching the filesystem or launching processes. All side effects live in `AiRunner` and `TeamAgentProcess`.
181
+ - **ProcessExecutor is injectable**: tests inject a stub executor; production uses `NodeProcessExecutor` (or the Bun pipe-process seam for team mode).
182
+ - **Events are opt-in**: `AiRunnerOptions.events` and `TeamOrchestratorOptions.events` accept an `EventBus<AgentEvents>` for structured observability. Without it, the runner is silent.
183
+ - **Team mode is composable**: `AgentSpec`, `TeamAgentProcess`, `MessageService`, and `TeamOrchestrator` are small building blocks. Downstream apps compose them into their own orchestration layer.
184
+
185
+ The package depends on `@gobing-ai/ts-runtime` for process execution, `@gobing-ai/ts-db` for team-mode inbox types, and `@gobing-ai/ts-infra` for structured logging and EventBus. The target agent CLIs are not bundled; install them separately in the host environment.
31
186
 
32
187
  ## Detect Installed Agents
33
188
 
@@ -78,12 +233,27 @@ console.log(result.stdout);
78
233
 
79
234
  `AiRunner` captures `stdout`, `stderr`, `exitCode`, optional termination `signal`, and `durationMs`. It does not throw on non-zero agent exits; callers decide how to handle failures.
80
235
 
236
+ Every invocation is logged through an injectable logger (`getLogger('ai-runner')` by default): one `debug` line per dispatch and an `error` line on any non-zero exit. Pass a custom `logger` to the constructor to route diagnostics elsewhere or silence them in tests.
237
+
238
+ ### Slash commands and command preview
239
+
240
+ `runSlashCommand()` translates a Claude-style `/plugin:command args` input into the target agent's dialect (via `translateSlashCommand()`) and dispatches it as a prompt. Non-slash input passes through unchanged.
241
+
242
+ ```ts
243
+ // For codex, "/review:pr 123" becomes "$review-pr 123" before dispatch.
244
+ await runner.runSlashCommand('codex', '/review:pr 123', { model: 'gpt-5' });
245
+ ```
246
+
247
+ `buildPromptCommand()` returns the resolved `{ command, args }` **without** executing it — useful for previewing, logging, or dry-running the exact argv a prompt would dispatch. It applies the same identity-preamble enrichment as `runPromptCommand()`.
248
+
249
+ ```ts
250
+ const { command, args } = runner.buildPromptCommand('pi', { input: 'ship it', mode: 'json' });
251
+ // command === 'pi', args === ['--no-session', '-p', 'ship it', '--mode', 'json']
252
+ ```
253
+
81
254
  ### Team identity preambles
82
255
 
83
- `PromptOptions` accepts optional team-mode fields. When any of `purpose`, `systemPrompt`, `taskId`,
84
- or non-empty `peers` is supplied, `AiRunner` prepends an identity preamble before dispatching the
85
- prompt through the selected agent shim. Existing callers that only pass `input`, `continue`, `model`,
86
- or `mode` are unchanged.
256
+ `PromptOptions` accepts optional team-mode fields. When any of `purpose`, `systemPrompt`, `taskId`, or non-empty `peers` is supplied, `AiRunner` prepends an identity preamble before dispatching the prompt through the selected agent shim.
87
257
 
88
258
  ```ts
89
259
  await runner.runPromptCommand('codex', {
@@ -95,8 +265,7 @@ await runner.runPromptCommand('codex', {
95
265
  });
96
266
  ```
97
267
 
98
- Use `buildIdentityPreamble()` directly when a host app needs to preview or inject the same context
99
- outside `AiRunner`:
268
+ Use `buildIdentityPreamble()` directly when a host app needs to preview or inject the same context outside `AiRunner`:
100
269
 
101
270
  ```ts
102
271
  import { buildIdentityPreamble } from '@gobing-ai/ts-ai-runner';
@@ -113,6 +282,43 @@ const preamble = buildIdentityPreamble({
113
282
  });
114
283
  ```
115
284
 
285
+ Use `getGitContext()` to auto-detect the current branch and dirty state:
286
+
287
+ ```ts
288
+ import { getGitContext } from '@gobing-ai/ts-ai-runner';
289
+
290
+ const gitBlock = getGitContext('/workspace/spur');
291
+ // "Git context:\nbranch: feat/team-mode\ndirty: 3 files"
292
+ ```
293
+
294
+ ## Observability
295
+
296
+ `AiRunner` and `TeamOrchestrator` emit typed events when an `EventBus<AgentEvents>` is provided:
297
+
298
+ ```ts
299
+ import { AiRunner } from '@gobing-ai/ts-ai-runner';
300
+ import { EventBus } from '@gobing-ai/ts-infra';
301
+ import type { AgentEvents } from '@gobing-ai/ts-ai-runner';
302
+
303
+ const bus = new EventBus<AgentEvents>();
304
+ bus.on('agent.invoke.start', (data) => console.log('starting', data.label));
305
+ bus.on('agent.invoke.exit', (data) => console.log('done', data.label, data.exitCode, data.durationMs));
306
+
307
+ const runner = new AiRunner({ events: bus });
308
+ ```
309
+
310
+ Available events:
311
+
312
+ | Event | When |
313
+ |-------|------|
314
+ | `agent.invoke.start` | Immediately before an agent CLI invocation starts |
315
+ | `agent.invoke.exit` | After an agent CLI invocation exits |
316
+ | `agent.started` | When a long-running team agent process starts |
317
+ | `agent.stopped` | When a long-running team agent process stops |
318
+ | `agent.message.sent` | When a message is sent to a team agent process |
319
+
320
+ `AiRunnerOptions.processEvents` accepts a separate `EventBus<AiRunnerProcessEvents>` for process-level events from the underlying executor.
321
+
116
322
  ## Inject a Process Executor
117
323
 
118
324
  For tests, dry runs, or sandboxed launchers, inject a `ProcessExecutor` from `@gobing-ai/ts-runtime`:
@@ -182,7 +388,7 @@ translateSlashCommand('pi', '/rd3:dev-fixall bun run check');
182
388
  // /skill:rd3-dev-fixall bun run check
183
389
  ```
184
390
 
185
- Non-slash input is returned unchanged.
391
+ Non-slash input is returned unchanged. Use `isClaudeStyleSlashCommand()` to test input before translation.
186
392
 
187
393
  ## Command Shims
188
394
 
@@ -197,17 +403,41 @@ const command = shim.getPromptCommand({ input: 'Summarize this repository' });
197
403
  console.log(command.command, command.args);
198
404
  ```
199
405
 
406
+ Each shim implements the `AgentShim` interface:
407
+
408
+ ```ts
409
+ interface AgentShim {
410
+ readonly name: AgentName;
411
+ readonly command: string;
412
+ readonly tier: 1 | 2;
413
+ getHelpCommand(): ShimCommand;
414
+ getVersionCommand(): ShimCommand;
415
+ getPromptCommand(options: PromptOptions): ShimCommand;
416
+ getAuthCommand(): ShimCommand | null;
417
+ }
418
+ ```
419
+
420
+ Agent-specific behavior:
421
+
422
+ | Agent | CLI | Tier | Auth check | Prompt flags |
423
+ |-------|-----|------|------------|--------------|
424
+ | `claude` | `claude` | 1 | `claude auth status` | `-p`, `--continue`, `--model`, `--output-format` |
425
+ | `codex` | `codex` | 1 | `codex login status` | `exec <prompt>`, `exec resume --last`, `-m`, `--json` |
426
+ | `gemini` | `gemini` | 1 | env-only | `-p`, `-r latest` (resume), `-m`, `-o` |
427
+ | `pi` | `pi` | 1 | `pi --list-models` | `--no-session`, `-p`, `-c` (resume), `--model`, `--mode` |
428
+ | `opencode` | `opencode` | 1 | `opencode providers` | `run`, `-c`, `-m`, `--format json` |
429
+ | `antigravity` | `agy` | 2 | env-only | `chat` |
430
+ | `openclaw` | `openclaw` | 2 | `openclaw health` | `agent --local -m` |
431
+
200
432
  This is the right layer for UI previews, audit logging, and custom launchers.
201
433
 
202
434
  ## Team Mode Primitives
203
435
 
204
- The team-mode APIs are intentionally small building blocks. They do not implement an HTTP API,
205
- dashboard, or product workflow; downstream apps compose them into their own orchestration layer.
436
+ The team-mode APIs are intentionally small building blocks. They do not implement an HTTP API, dashboard, or product workflow; downstream apps compose them into their own orchestration layer.
206
437
 
207
438
  ### Agent specs
208
439
 
209
- Agent specs define agents as config. The built-in parser supports the repository's constrained YAML
210
- subset: scalars, arrays, nested objects, and no anchors/tags/multiline scalars.
440
+ Agent specs define agents as config. The built-in parser supports the repository's constrained YAML subset: scalars, arrays, nested objects, and no anchors/tags/multiline scalars.
211
441
 
212
442
  ```ts
213
443
  import { loadAgentSpecs, saveAgentSpec } from '@gobing-ai/ts-ai-runner';
@@ -229,12 +459,11 @@ await saveAgentSpec(
229
459
  const specs = loadAgentSpecs('./agents');
230
460
  ```
231
461
 
232
- `validateAgentId()` enforces lowercase agent ids with alphanumeric, `_`, and `-` characters.
462
+ `validateAgentId()` enforces lowercase agent ids with alphanumeric, `_`, and `-` characters (2–64 chars). `deleteAgentSpec()` removes a spec file by id.
233
463
 
234
464
  ### Durable messages
235
465
 
236
- `MessageService` wraps `InboxMessageDao` from `@gobing-ai/ts-db/inbox`. It owns no subprocess
237
- behavior; it only persists, drains, marks delivery/failure, and formats messages.
466
+ `MessageService` wraps `InboxMessageDao` from `@gobing-ai/ts-db/inbox`. It owns no subprocess behavior; it only persists, drains, marks delivery/failure, and formats messages.
238
467
 
239
468
  ```ts
240
469
  import { InboxMessageDao } from '@gobing-ai/ts-db/inbox';
@@ -251,10 +480,11 @@ for (const msg of pending) {
251
480
  }
252
481
  ```
253
482
 
483
+ `MessageService` also exposes `fail(msgId, error)`, `inbox(toId, limit?, offset?)`, and `countPending(toId)`.
484
+
254
485
  ### Persistent agent processes
255
486
 
256
- `TeamAgentProcess` wraps a long-running agent subprocess using the runtime pipe-process seam. It
257
- supports start/stop, stdin sends, stdout/stderr subscriptions, status, pid, and exit-code queries.
487
+ `TeamAgentProcess` wraps a long-running agent subprocess using the runtime pipe-process seam. It supports start/stop, stdin sends, stdout/stderr subscriptions, status, pid, and exit-code queries.
258
488
 
259
489
  ```ts
260
490
  import { TeamAgentProcess, type AgentSpec } from '@gobing-ai/ts-ai-runner';
@@ -286,10 +516,7 @@ unsubscribe();
286
516
 
287
517
  ### Team orchestrator
288
518
 
289
- `TeamOrchestrator` connects specs, shims, processes, and messages. On start it loads an agent spec,
290
- builds the agent command through the matching shim, starts the process, drains pending inbox messages,
291
- and injects them live. `sendMessage()` always persists first, then injects immediately when the target
292
- agent is running.
519
+ `TeamOrchestrator` connects specs, shims, processes, and messages. On start it loads an agent spec, builds the agent command through the matching shim, starts the process, drains pending inbox messages, and injects them live. `sendMessage()` always persists first, then injects immediately when the target agent is running.
293
520
 
294
521
  ```ts
295
522
  import { InboxMessageDao } from '@gobing-ai/ts-db/inbox';
@@ -306,10 +533,87 @@ console.log(team.getAgentStatus('coder')); // running
306
533
  await team.stopAll();
307
534
  ```
308
535
 
536
+ The orchestrator also provides `restartAgent(id)`, `getRunningAgents()`, `getPeerSpecs(workspace, excludeId?)`, and `on(event, listener)` for event subscription.
537
+
538
+ ## Adding a New Coding Agent
539
+
540
+ To add support for a new coding agent (e.g. `amp`), follow these steps:
541
+
542
+ ### 1. Add the shim
543
+
544
+ Edit `src/agents/shims.ts`. Add a new constant implementing the `AgentShim` interface:
545
+
546
+ ```ts
547
+ const ampShim: AgentShim = {
548
+ name: 'amp',
549
+ command: 'amp',
550
+ tier: 1, // or 2 if gateway/TUI-constrained
551
+ getHelpCommand: () => ({ command: 'amp', args: ['--help'] }),
552
+ getVersionCommand: () => ({ command: 'amp', args: ['--version'] }),
553
+ getPromptCommand: (options) => {
554
+ const args = ['run', options.input ?? ''];
555
+ if (options.continue === true) args.push('--resume');
556
+ if (options.model !== undefined) args.push('--model', options.model);
557
+ if ((options.mode ?? 'text') === 'json') args.push('--json');
558
+ return { command: 'amp', args };
559
+ },
560
+ getAuthCommand: () => ({ command: 'amp', args: ['auth', 'status'] }),
561
+ };
562
+ ```
563
+
564
+ ### 2. Register the shim
565
+
566
+ In the same file, add the new agent to these three places:
567
+
568
+ ```ts
569
+ // 1. The AgentName union type
570
+ export type AgentName = 'claude' | 'codex' | 'gemini' | 'pi' | 'opencode' | 'antigravity' | 'openclaw' | 'amp';
571
+
572
+ // 2. The AGENT_SHIMS registry
573
+ export const AGENT_SHIMS: Readonly<Record<AgentName, AgentShim>> = {
574
+ // ...existing entries...
575
+ amp: ampShim,
576
+ };
577
+
578
+ // 3. DISPLAY_ORDER (determines doctor/detector output order)
579
+ export const DISPLAY_ORDER: readonly AgentName[] = [
580
+ 'claude', 'codex', 'gemini', 'pi', 'opencode', 'antigravity', 'openclaw', 'amp',
581
+ ];
582
+ ```
583
+
584
+ If the agent is tier 1, add it to `TIER1_PRIORITY`. If tier 2 (gateway/TUI-constrained), add it to `TIER2_AGENTS`.
585
+
586
+ ### 3. Add slash-command dialect (if needed)
587
+
588
+ If the agent uses a different slash-command syntax than the default `/{plugin}-{command} args` mapping, add a case to `translateSlashCommand()` in `src/slash-command.ts`.
589
+
590
+ ### 4. Add auth detection (if applicable)
591
+
592
+ If the agent supports an auth-status command, `getAuthCommand()` already returns it and `DoctorRunner` will probe it automatically. For env-only or file-based auth, add a pattern to `AUTH_PATTERNS` in `src/doctor-runner.ts`.
593
+
594
+ ### 5. Add tests
595
+
596
+ - **Shim tests**: add cases to `tests/agents/` verifying command construction for help, version, prompt (with and without options), and auth.
597
+ - **Detector tests**: verify `detectOne('amp')` handles version output and error cases.
598
+ - **Doctor tests**: verify auth probe for the new agent.
599
+
600
+ ### Summary checklist
601
+
602
+ | Step | File | What to change |
603
+ |------|------|----------------|
604
+ | Shim | `src/agents/shims.ts` | Add `AgentShim` impl, update `AgentName`, `AGENT_SHIMS`, `DISPLAY_ORDER` |
605
+ | Tier | `src/agents/shims.ts` | Add to `TIER1_PRIORITY` or `TIER2_AGENTS` |
606
+ | Slash | `src/slash-command.ts` | Add case if dialect differs from default |
607
+ | Auth | `src/doctor-runner.ts` | Add `AUTH_PATTERNS` entry if file/env-based |
608
+ | Tests | `tests/` | Shim, detector, and doctor coverage |
609
+
610
+ After these changes, the new agent is automatically available to `AiRunner`, `AgentDetector`, `DoctorRunner`, `TeamOrchestrator`, and all downstream consumers — no further registration needed.
611
+
309
612
  ## Boundary Notes
310
613
 
311
614
  - This package is a command adapter, not an agent orchestration framework.
312
615
  - It does not install agent CLIs or manage credentials.
313
616
  - It does not parse agent responses beyond process result capture.
314
- - It keeps subprocess launching behind `ProcessExecutor` / `PipeProcessSpawner`, so tests can stay deterministic.
617
+ - It keeps subprocess launching behind `ProcessExecutor` / `PipeProcess`, so tests can stay deterministic.
315
618
  - Team-mode persistence is delegated to `@gobing-ai/ts-db/inbox`; host apps own migrations and adapter lifecycle.
619
+ - Platform APIs (`node:fs`, `node:path`, `Bun.spawn`, etc.) are confined to `@gobing-ai/ts-runtime` per ADR-011. This package accesses them through the runtime's `FileSystem`, `ProcessExecutor`, and path utilities.
@@ -30,5 +30,6 @@ export declare class AgentDetector {
30
30
  /** Probe one agent by name. */
31
31
  detectOne(agent: string): Promise<DetectedAgent>;
32
32
  private parseResult;
33
+ private detectChannels;
33
34
  }
34
35
  //# sourceMappingURL=agent-detector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-detector.d.ts","sourceRoot":"","sources":["../src/agent-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EAAuB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC1B,wBAAwB;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wCAAwC;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACjC,gCAAgC;IAChC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,6EAA6E;AAC7E,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,GAAE,oBAAyB;IAK9C,iDAAiD;IAC3C,SAAS,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAI3C,+BAA+B;IACzB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAStD,OAAO,CAAC,WAAW;CAyBtB"}
1
+ {"version":3,"file":"agent-detector.d.ts","sourceRoot":"","sources":["../src/agent-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EAAuB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC1B,wBAAwB;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wCAAwC;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACjC,gCAAgC;IAChC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,6EAA6E;AAC7E,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,GAAE,oBAAyB;IAK9C,iDAAiD;IAC3C,SAAS,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAI3C,+BAA+B;IACzB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAStD,OAAO,CAAC,WAAW;IA8BnB,OAAO,CAAC,cAAc;CAIzB"}
@@ -32,24 +32,32 @@ export class AgentDetector {
32
32
  if (lower.includes('command not found') || lower.includes('enoent') || lower.includes('not recognized')) {
33
33
  return unavailable(agent, `${command}: command not found`);
34
34
  }
35
- if (result.signal !== undefined || result.exitCode === null) {
36
- return unavailable(agent, result.signal ?? 'Process timed out');
35
+ if (result.signal !== undefined) {
36
+ return unavailable(agent, `Terminated by signal: ${result.signal}`);
37
+ }
38
+ if (result.exitCode === null) {
39
+ return unavailable(agent, 'Process did not produce an exit code');
37
40
  }
38
41
  if (result.exitCode !== 0) {
39
- return unavailable(agent, `Non-zero exit code ${result.exitCode}: ${result.stderr.slice(0, 200)}`);
42
+ return unavailable(agent, `Non-zero exit code: ${result.exitCode}. stderr: ${result.stderr.slice(0, 200)}`);
40
43
  }
41
44
  const match = VERSION_PATTERN.exec(output);
42
45
  if (match?.groups?.version === undefined) {
43
46
  return unavailable(agent, 'Could not parse version output');
44
47
  }
48
+ const version = output.split('\n')[0]?.trim() || match.groups.version;
45
49
  return {
46
50
  name: agent,
47
51
  installed: true,
48
- version: match.groups.version,
49
- channels: [],
52
+ version,
53
+ channels: this.detectChannels(agent, output),
50
54
  error: null,
51
55
  };
52
56
  }
57
+ detectChannels(_agent, _output) {
58
+ // Phase 2 hook: parse per-agent channel/model output here when shims expose it.
59
+ return [];
60
+ }
53
61
  }
54
62
  /** Build an "unavailable" detection result for an agent with the given error. */
55
63
  function unavailable(name, error) {
@@ -1,4 +1,5 @@
1
1
  import { type SyncFileSystem } from '@gobing-ai/ts-runtime';
2
+ /** Specification for an AI agent defined in the configuration directory. */
2
3
  export interface AgentSpec {
3
4
  id: string;
4
5
  name: string;
@@ -9,11 +10,16 @@ export interface AgentSpec {
9
10
  config: Record<string, unknown>;
10
11
  autoStart?: boolean;
11
12
  }
13
+ /** Validation error thrown when agent spec data is malformed or invalid. */
12
14
  export declare class ValueError extends Error {
13
15
  constructor(message: string);
14
16
  }
17
+ /** Validate that `id` matches the agent ID format: 2-64 chars, lowercase alphanumeric with `_` or `-`. Returns the id on success, throws `ValueError` otherwise. */
15
18
  export declare function validateAgentId(id: string): string;
19
+ /** Load and validate all YAML agent spec files from `configDir`. Throws `ValueError` on parse failures or duplicate IDs. */
16
20
  export declare function loadAgentSpecs(configDir: string, fs?: SyncFileSystem): AgentSpec[];
21
+ /** Serialize `spec` to YAML and write it to `configDir/<id>.yaml`. Creates the directory if missing. */
17
22
  export declare function saveAgentSpec(spec: AgentSpec, configDir: string, fs?: SyncFileSystem): Promise<void>;
23
+ /** Remove the YAML file for agent `id` from `configDir`. Does nothing if the file doesn't exist. */
18
24
  export declare function deleteAgentSpec(id: string, configDir: string, fs?: SyncFileSystem): Promise<void>;
19
25
  //# sourceMappingURL=agent-spec.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-spec.d.ts","sourceRoot":"","sources":["../src/agent-spec.ts"],"names":[],"mappings":"AACA,OAAO,EAAuC,KAAK,cAAc,EAAuB,MAAM,uBAAuB,CAAC;AAEtH,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,UAAW,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAI9B;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,GAAE,cAAyC,GAAG,SAAS,EAAE,CAY5G;AAED,wBAAsB,aAAa,CAC/B,IAAI,EAAE,SAAS,EACf,SAAS,EAAE,MAAM,EACjB,EAAE,GAAE,cAAyC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,eAAe,CACjC,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,EAAE,GAAE,cAAyC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAGf"}
1
+ {"version":3,"file":"agent-spec.d.ts","sourceRoot":"","sources":["../src/agent-spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,cAAc,EAEtB,MAAM,uBAAuB,CAAC;AAE/B,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,4EAA4E;AAC5E,qBAAa,UAAW,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAI9B;AAED,oKAAoK;AACpK,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,4HAA4H;AAC5H,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,GAAE,cAAyC,GAAG,SAAS,EAAE,CAY5G;AAED,wGAAwG;AACxG,wBAAsB,aAAa,CAC/B,IAAI,EAAE,SAAS,EACf,SAAS,EAAE,MAAM,EACjB,EAAE,GAAE,cAAyC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,oGAAoG;AACpG,wBAAsB,eAAe,CACjC,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,EAAE,GAAE,cAAyC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAGf"}
@@ -1,22 +1,24 @@
1
- import { basename, join } from 'node:path';
2
- import { NodeSyncFileSystem, parseYamlObject, stringifyYamlObject } from '@gobing-ai/ts-runtime';
1
+ import { basenamePath, joinPath, NodeSyncFileSystem, parseYamlObject, stringifyYamlObject, } from '@gobing-ai/ts-runtime';
2
+ /** Validation error thrown when agent spec data is malformed or invalid. */
3
3
  export class ValueError extends Error {
4
4
  constructor(message) {
5
5
  super(message);
6
6
  this.name = 'ValueError';
7
7
  }
8
8
  }
9
+ /** Validate that `id` matches the agent ID format: 2-64 chars, lowercase alphanumeric with `_` or `-`. Returns the id on success, throws `ValueError` otherwise. */
9
10
  export function validateAgentId(id) {
10
11
  if (!/^[a-z][a-z0-9_-]{1,63}$/.test(id)) {
11
12
  throw new ValueError(`Invalid agent id "${id}": expected 2-64 chars, lowercase alphanumeric, "_" or "-"`);
12
13
  }
13
14
  return id;
14
15
  }
16
+ /** Load and validate all YAML agent spec files from `configDir`. Throws `ValueError` on parse failures or duplicate IDs. */
15
17
  export function loadAgentSpecs(configDir, fs = new NodeSyncFileSystem()) {
16
18
  const entries = safeReadDir(configDir, fs)
17
19
  .filter((entry) => entry.endsWith('.yaml') || entry.endsWith('.yml'))
18
20
  .sort();
19
- const specs = entries.map((entry) => parseAgentSpec(fs.readFile(join(configDir, entry)), entry));
21
+ const specs = entries.map((entry) => parseAgentSpec(fs.readFile(joinPath(configDir, entry)), entry));
20
22
  const seen = new Set();
21
23
  for (const spec of specs) {
22
24
  validateAgentId(spec.id);
@@ -26,14 +28,16 @@ export function loadAgentSpecs(configDir, fs = new NodeSyncFileSystem()) {
26
28
  }
27
29
  return specs;
28
30
  }
31
+ /** Serialize `spec` to YAML and write it to `configDir/<id>.yaml`. Creates the directory if missing. */
29
32
  export async function saveAgentSpec(spec, configDir, fs = new NodeSyncFileSystem()) {
30
33
  validateAgentId(spec.id);
31
34
  fs.mkdir(configDir);
32
- fs.writeFile(join(configDir, `${spec.id}.yaml`), serializeAgentSpec(spec));
35
+ fs.writeFile(joinPath(configDir, `${spec.id}.yaml`), serializeAgentSpec(spec));
33
36
  }
37
+ /** Remove the YAML file for agent `id` from `configDir`. Does nothing if the file doesn't exist. */
34
38
  export async function deleteAgentSpec(id, configDir, fs = new NodeSyncFileSystem()) {
35
39
  validateAgentId(id);
36
- fs.unlink(join(configDir, `${id}.yaml`));
40
+ fs.unlink(joinPath(configDir, `${id}.yaml`));
37
41
  }
38
42
  function safeReadDir(configDir, fs = new NodeSyncFileSystem()) {
39
43
  try {
@@ -76,21 +80,21 @@ function serializeAgentSpec(spec) {
76
80
  function requireString(source, key, fileName) {
77
81
  const value = source[key];
78
82
  if (typeof value !== 'string' || value.trim() === '') {
79
- throw new ValueError(`${basename(fileName)}: "${key}" must be a non-empty string`);
83
+ throw new ValueError(`${basenamePath(fileName)}: "${key}" must be a non-empty string`);
80
84
  }
81
85
  return value;
82
86
  }
83
87
  function requireStringArray(source, key, fileName) {
84
88
  const value = source[key];
85
89
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
86
- throw new ValueError(`${basename(fileName)}: "${key}" must be a string array`);
90
+ throw new ValueError(`${basenamePath(fileName)}: "${key}" must be a string array`);
87
91
  }
88
92
  return value;
89
93
  }
90
94
  function requireRecord(source, key, fileName) {
91
95
  const value = source[key];
92
96
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
93
- throw new ValueError(`${basename(fileName)}: "${key}" must be an object`);
97
+ throw new ValueError(`${basenamePath(fileName)}: "${key}" must be an object`);
94
98
  }
95
99
  return value;
96
100
  }