@hilbras/omninode 0.3.0 → 0.4.0

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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v0.3.0 — Phase 2: OmniHilbras Integration** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
21
+ **v0.4.0 — Phase 3: CLI Agent System** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
22
22
  for the full plan and roadmap).
23
23
 
24
24
  What exists today:
@@ -35,6 +35,15 @@ What exists today:
35
35
  - Provider factory mapping config to adapters; `omnihilbras` gets a
36
36
  dedicated adapter implementing the plan's connect flow —
37
37
  Connect → Authenticate → Fetch Models → Validate Models → Register
38
+ - **Agent system** (`src/agents/`):
39
+ - Process adapter: spawns CLI agents as child processes with lifecycle
40
+ management — environment, working directory, exit codes, stderr logs,
41
+ per-task timeouts (SIGTERM → SIGKILL) and cancellation
42
+ - Three input modes: `stdin` (composed task text on stdin), `arg` (prompt as
43
+ last argument) and `protocol` (the §21 JSON-lines task protocol —
44
+ `TASK`/`ROLE`/`CONTEXT`/`INSTRUCTION` envelopes in, `REPORT`/`COMPLETION`/
45
+ `ERROR` messages out, parsed into structured reports)
46
+ - Agent factory and registry (`createAgent`, `AgentRegistry`)
38
47
  - Typed error hierarchy (`src/errors/`)
39
48
  - Leveled logger with pluggable sink (`src/logger/`)
40
49
  - Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
@@ -76,6 +85,14 @@ omninode agent list
76
85
  omninode role list
77
86
  ```
78
87
 
88
+ Register and verify a CLI agent (anything executable works — `opencode`,
89
+ `kimi`, a custom script):
90
+
91
+ ```bash
92
+ omninode agent add --name opencode --command opencode --input-mode arg
93
+ omninode agent test opencode # sends a trivial task over the real adapter
94
+ ```
95
+
79
96
  See [omninode.yaml.example](omninode.yaml.example) for a full configuration.
80
97
 
81
98
  ### Configuration
@@ -107,8 +124,8 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
107
124
  | --- | --- |
108
125
  | v0.1.0 | Foundation |
109
126
  | v0.2.0 | Provider System |
110
- | **v0.3.0** | **OmniHilbras Integration (current)** |
111
- | v0.4.0 | Agent System |
127
+ | v0.3.0 | OmniHilbras Integration (optional, like every provider) |
128
+ | **v0.4.0** | **CLI Agent System (current)** |
112
129
  | v0.5.0 | Roles & Tasks |
113
130
  | v0.6.0 | Pipeline Engine |
114
131
  | v0.7.0 | Multi-AI Reports |
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "0.3.0";
9
+ var OMNINODE_VERSION = "0.4.0";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -29,6 +29,16 @@ var ProviderError = class extends OmniNodeError {
29
29
  super(code, message, options);
30
30
  }
31
31
  };
32
+ var AgentError = class extends OmniNodeError {
33
+ constructor(code, message, options = {}) {
34
+ super(code, message, options);
35
+ }
36
+ };
37
+ var ProtocolError = class extends OmniNodeError {
38
+ constructor(message, options = {}) {
39
+ super("PROTOCOL_VIOLATION", message, options);
40
+ }
41
+ };
32
42
  function isOmniNodeError(value) {
33
43
  return value instanceof OmniNodeError;
34
44
  }
@@ -55,6 +65,8 @@ var agentConfigSchema = z.object({
55
65
  args: z.array(z.string()).optional(),
56
66
  cwd: z.string().optional(),
57
67
  env: z.record(z.string()).optional(),
68
+ input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
69
+ timeout_ms: z.number().int().positive().optional(),
58
70
  metadata: z.record(z.unknown()).optional()
59
71
  }).strict();
60
72
  var roleConfigSchema = z.object({
@@ -166,6 +178,8 @@ function toAgentConfig(raw) {
166
178
  args: raw.args,
167
179
  cwd: raw.cwd,
168
180
  env: raw.env,
181
+ inputMode: raw.input_mode,
182
+ timeoutMs: raw.timeout_ms,
169
183
  metadata: raw.metadata
170
184
  };
171
185
  }
@@ -243,10 +257,6 @@ function registerProjectCommands(program) {
243
257
  project.command("init").description("Alias of `omninode init`.").option("-f, --force", "Overwrite an existing omninode.yaml.").option("-n, --name <name>", "Project name to write into the configuration.", "My Project").action(async (options) => runInit(options));
244
258
  }
245
259
 
246
- // src/cli/commands/providers.ts
247
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
248
- import { parseDocument } from "yaml";
249
-
250
260
  // src/providers/auth.ts
251
261
  function resolveApiKey(config, env = process.env) {
252
262
  if (!config.apiKeyEnvVar) return void 0;
@@ -610,12 +620,36 @@ var ModelRegistry = class {
610
620
  };
611
621
 
612
622
  // src/cli/commands/shared.ts
623
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
624
+ import { parseDocument } from "yaml";
613
625
  function notImplemented(command, when) {
614
626
  return new OmniNodeError(
615
627
  "NOT_IMPLEMENTED",
616
628
  `${command} is planned for ${when}. Edit omninode.yaml directly for now.`
617
629
  );
618
630
  }
631
+ function appendToConfigList(listKey, item) {
632
+ const configPath = findConfigFile();
633
+ if (!configPath) {
634
+ throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
635
+ }
636
+ const doc = parseDocument(readFileSync2(configPath, "utf8"));
637
+ if (doc.getIn(["project", listKey]) === void 0) {
638
+ doc.setIn(["project", listKey], [item]);
639
+ } else {
640
+ doc.addIn(["project", listKey], item);
641
+ }
642
+ const validated = appConfigSchema.safeParse(doc.toJS());
643
+ if (!validated.success) {
644
+ const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
645
+ throw new ConfigError(
646
+ "CONFIG_INVALID",
647
+ `Editing ${configPath} would produce an invalid configuration: ${issues}`
648
+ );
649
+ }
650
+ writeFileSync2(configPath, doc.toString(), "utf8");
651
+ return configPath;
652
+ }
619
653
 
620
654
  // src/cli/commands/providers.ts
621
655
  function registerProviderCommands(program) {
@@ -675,25 +709,11 @@ async function addProvider(options) {
675
709
  if (existing.project.providers.some((p) => p.name === newProvider.name)) {
676
710
  throw new OmniNodeError(
677
711
  "CLI_USAGE",
678
- `Provider "${newProvider.name}" already exists in ${configPath}.`
679
- );
680
- }
681
- const doc = parseDocument(readFileSync2(configPath, "utf8"));
682
- if (doc.getIn(["project", "providers"]) === void 0) {
683
- doc.setIn(["project", "providers"], [newProvider]);
684
- } else {
685
- doc.addIn(["project", "providers"], newProvider);
686
- }
687
- const validated = appConfigSchema.safeParse(doc.toJS());
688
- if (!validated.success) {
689
- const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
690
- throw new ConfigError(
691
- "CONFIG_INVALID",
692
- `Editing ${configPath} would produce an invalid configuration: ${issues}`
712
+ `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
693
713
  );
694
714
  }
695
- writeFileSync2(configPath, doc.toString(), "utf8");
696
- console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${configPath}.`);
715
+ const writtenPath = appendToConfigList("providers", newProvider);
716
+ console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
697
717
  if (newProvider.api_key_env_var) {
698
718
  console.log(
699
719
  `Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
@@ -790,26 +810,354 @@ function registerModelsCommand(program) {
790
810
  });
791
811
  }
792
812
 
813
+ // src/agents/process/index.ts
814
+ import { spawn } from "child_process";
815
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
816
+ var KILL_GRACE_MS = 5e3;
817
+ function composeTaskText(input) {
818
+ const sections = [`# Objective
819
+ ${input.objective}`];
820
+ if (input.role) {
821
+ const lines = ["# Role", `${input.role.name} (${input.role.id})`];
822
+ if (input.role.responsibilities.length > 0) {
823
+ lines.push("", "Responsibilities:", ...input.role.responsibilities.map((r) => `- ${r}`));
824
+ }
825
+ if (input.role.expectedOutputs.length > 0) {
826
+ lines.push("", "Expected outputs:", ...input.role.expectedOutputs.map((o) => `- ${o}`));
827
+ }
828
+ sections.push(lines.join("\n"));
829
+ }
830
+ if (input.context) sections.push(`# Context
831
+ ${input.context}`);
832
+ if (input.instruction) sections.push(`# Instruction
833
+ ${input.instruction}`);
834
+ return sections.join("\n\n");
835
+ }
836
+ function buildProtocolMessages(input, agentName) {
837
+ const meta = {
838
+ taskId: input.taskId,
839
+ agent: agentName,
840
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
841
+ };
842
+ const messages = [
843
+ { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
844
+ ];
845
+ if (input.role) messages.push({ type: "ROLE", payload: input.role });
846
+ if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
847
+ if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
848
+ return messages.map((message) => JSON.stringify({ meta, message }));
849
+ }
850
+ var ProcessAgent = class {
851
+ config;
852
+ info;
853
+ log;
854
+ running = /* @__PURE__ */ new Map();
855
+ constructor(config, log = logger) {
856
+ if (!config.command) {
857
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${config.name}" has no command configured.`);
858
+ }
859
+ this.config = config;
860
+ this.log = log.child({ agent: config.name });
861
+ this.info = {
862
+ name: config.name,
863
+ integration: "process",
864
+ command: config.command,
865
+ cwd: config.cwd,
866
+ status: "ready",
867
+ capabilities: [config.inputMode ?? "stdin"]
868
+ };
869
+ }
870
+ async run(input) {
871
+ const command = this.config.command;
872
+ if (!command) {
873
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
874
+ }
875
+ const mode = this.config.inputMode ?? "stdin";
876
+ const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
877
+ return new Promise((resolve, reject) => {
878
+ const child = spawn(
879
+ command,
880
+ [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
881
+ {
882
+ cwd: this.config.cwd,
883
+ env: { ...process.env, ...this.config.env },
884
+ stdio: ["pipe", "pipe", "pipe"]
885
+ }
886
+ );
887
+ this.running.set(input.taskId, child);
888
+ let stdout = "";
889
+ let stderr = "";
890
+ let timedOut = false;
891
+ let settled = false;
892
+ const timer = setTimeout(() => {
893
+ timedOut = true;
894
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
895
+ child.kill("SIGTERM");
896
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
897
+ }, timeoutMs);
898
+ const settle = (finish) => {
899
+ if (settled) return;
900
+ settled = true;
901
+ clearTimeout(timer);
902
+ this.running.delete(input.taskId);
903
+ finish();
904
+ };
905
+ child.stdout?.on("data", (chunk) => {
906
+ stdout += chunk.toString();
907
+ });
908
+ child.stderr?.on("data", (chunk) => {
909
+ stderr += chunk.toString();
910
+ });
911
+ child.stdin?.on("error", () => {
912
+ });
913
+ if (mode !== "arg") {
914
+ const payload = mode === "protocol" ? buildProtocolMessages(input, this.config.name).join("\n") + "\n" : composeTaskText(input) + "\n";
915
+ child.stdin?.write(payload);
916
+ child.stdin?.end();
917
+ }
918
+ child.on("error", (error) => {
919
+ settle(() => {
920
+ if (error.code === "ENOENT") {
921
+ reject(
922
+ new AgentError(
923
+ "AGENT_NOT_FOUND",
924
+ `Agent "${this.config.name}": command "${command}" was not found. Is it installed and on PATH?`,
925
+ { cause: error }
926
+ )
927
+ );
928
+ return;
929
+ }
930
+ reject(
931
+ new AgentError("AGENT_FAILED", `Agent "${this.config.name}" failed to start: ${error.message}.`, {
932
+ cause: error
933
+ })
934
+ );
935
+ });
936
+ });
937
+ child.on("close", (code, signal) => {
938
+ settle(() => {
939
+ if (timedOut) {
940
+ reject(
941
+ new AgentError(
942
+ "AGENT_TIMEOUT",
943
+ `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
944
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
945
+ )
946
+ );
947
+ return;
948
+ }
949
+ const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
950
+ if (mode === "protocol") {
951
+ try {
952
+ resolve(
953
+ interpretProtocolRun({
954
+ input,
955
+ agentName: this.config.name,
956
+ stdout,
957
+ exitCode: code,
958
+ logs
959
+ })
960
+ );
961
+ } catch (error) {
962
+ reject(error);
963
+ }
964
+ return;
965
+ }
966
+ const trimmed = stdout.trim();
967
+ resolve({
968
+ taskId: input.taskId,
969
+ status: code === 0 ? "completed" : "failed",
970
+ summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
971
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
972
+ exitCode: code ?? void 0,
973
+ logs,
974
+ error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
975
+ });
976
+ });
977
+ });
978
+ });
979
+ }
980
+ /** Ask the agent to stop current work (SIGTERM). */
981
+ async cancel(taskId) {
982
+ const child = this.running.get(taskId);
983
+ if (child) child.kill("SIGTERM");
984
+ }
985
+ runningTaskIds() {
986
+ return [...this.running.keys()];
987
+ }
988
+ };
989
+ function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
990
+ const reports = [];
991
+ let summary;
992
+ let error;
993
+ let sawProtocolMessage = false;
994
+ for (const line of stdout.split("\n")) {
995
+ const trimmed2 = line.trim();
996
+ if (trimmed2.length === 0) continue;
997
+ const message = parseAgentMessage(trimmed2);
998
+ if (!message) continue;
999
+ sawProtocolMessage = true;
1000
+ if (message.type === "REPORT") {
1001
+ const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
1002
+ if (report) reports.push(report);
1003
+ } else if (message.type === "COMPLETION") {
1004
+ const payload = message.payload;
1005
+ if (typeof payload?.summary === "string") summary = payload.summary;
1006
+ } else if (message.type === "ERROR") {
1007
+ const payload = message.payload;
1008
+ const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
1009
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1010
+ error = `Agent error${code}: ${detail}`;
1011
+ }
1012
+ }
1013
+ if (!sawProtocolMessage && exitCode === 0) {
1014
+ throw new ProtocolError(
1015
+ `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
1016
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
1017
+ );
1018
+ }
1019
+ const trimmed = stdout.trim();
1020
+ return {
1021
+ taskId: input.taskId,
1022
+ status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
1023
+ summary: summary ?? reports[0]?.summary,
1024
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1025
+ exitCode: exitCode ?? void 0,
1026
+ logs,
1027
+ reports: reports.length > 0 ? reports : void 0,
1028
+ error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1029
+ };
1030
+ }
1031
+ function parseAgentMessage(line) {
1032
+ try {
1033
+ const parsed = JSON.parse(line);
1034
+ if (typeof parsed.type === "string") {
1035
+ return { type: parsed.type, payload: parsed.payload };
1036
+ }
1037
+ const inner = parsed.message;
1038
+ if (inner && typeof inner.type === "string") {
1039
+ return { type: inner.type, payload: inner.payload };
1040
+ }
1041
+ } catch {
1042
+ }
1043
+ return void 0;
1044
+ }
1045
+ function normalizeReport(payload, taskId, agentName, index) {
1046
+ if (payload === null || typeof payload !== "object") return void 0;
1047
+ const raw = payload;
1048
+ if (typeof raw.summary !== "string") return void 0;
1049
+ return {
1050
+ id: typeof raw.id === "string" ? raw.id : `${taskId}-report-${index}`,
1051
+ taskId: typeof raw.taskId === "string" ? raw.taskId : taskId,
1052
+ agent: typeof raw.agent === "string" ? raw.agent : agentName,
1053
+ summary: raw.summary,
1054
+ findings: Array.isArray(raw.findings) ? raw.findings : [],
1055
+ recommendations: Array.isArray(raw.recommendations) ? raw.recommendations : [],
1056
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString()
1057
+ };
1058
+ }
1059
+ function firstLine(text, maxLength = 200) {
1060
+ const line = text.split("\n", 1)[0] ?? text;
1061
+ return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1062
+ }
1063
+
1064
+ // src/agents/factory.ts
1065
+ function createAgent(config) {
1066
+ if (config.integration === "process") {
1067
+ return new ProcessAgent(config);
1068
+ }
1069
+ if (config.integration === "native") {
1070
+ throw new OmniNodeError(
1071
+ "NOT_IMPLEMENTED",
1072
+ `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1073
+ );
1074
+ }
1075
+ throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
1076
+ }
1077
+
793
1078
  // src/cli/commands/agents.ts
1079
+ var collectArgs = (value, previous) => [...previous, value];
794
1080
  function registerAgentCommands(program) {
795
1081
  const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
796
1082
  agent.command("list").description("List configured agents.").action(async () => {
797
1083
  const config = loadConfig();
798
1084
  const agents = config.project.agents;
799
1085
  if (agents.length === 0) {
800
- console.log("No agents configured. Add one under `project.agents` in omninode.yaml.");
1086
+ console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
801
1087
  return;
802
1088
  }
803
1089
  for (const agent2 of agents) {
804
1090
  const command = agent2.command ?? "(no command)";
805
1091
  console.log(
806
- `${agent2.name} integration=${agent2.integration} command=${command}`
1092
+ `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
807
1093
  );
808
1094
  }
809
1095
  });
810
- agent.command("add").description("Register an agent (planned for Phase 3 \u2014 CLI Agent System).").action(() => {
811
- throw notImplemented("omninode agent add", "Phase 3 (CLI Agent System)");
1096
+ agent.command("add").description(
1097
+ "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
1098
+ ).requiredOption("--name <name>", "Unique agent name.").requiredOption("--command <command>", "Executable command, e.g. opencode").option("--arg <value>", "Additional command argument (repeatable).", collectArgs, []).option("--cwd <dir>", "Working directory for the agent process.").option(
1099
+ "--input-mode <mode>",
1100
+ "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
1101
+ "stdin"
1102
+ ).option(
1103
+ "--timeout-ms <ms>",
1104
+ "Per-task timeout in milliseconds.",
1105
+ (value) => Number(value)
1106
+ ).action(async (options) => addAgent(options));
1107
+ agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
1108
+ }
1109
+ async function addAgent(options) {
1110
+ const candidate = agentConfigSchema.safeParse({
1111
+ name: options.name,
1112
+ type: "cli",
1113
+ command: options.command,
1114
+ ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
1115
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1116
+ input_mode: options.inputMode,
1117
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
812
1118
  });
1119
+ if (!candidate.success) {
1120
+ const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
1121
+ throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
1122
+ }
1123
+ const newAgent = candidate.data;
1124
+ const existing = loadConfig();
1125
+ if (existing.project.agents.some((a) => a.name === newAgent.name)) {
1126
+ throw new OmniNodeError(
1127
+ "CLI_USAGE",
1128
+ `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
1129
+ );
1130
+ }
1131
+ const configPath = appendToConfigList("agents", newAgent);
1132
+ console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
1133
+ console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
1134
+ }
1135
+ async function testAgent(name) {
1136
+ const config = loadConfig();
1137
+ const agentConfig = config.project.agents.find((a) => a.name === name);
1138
+ if (!agentConfig) {
1139
+ const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
1140
+ throw new AgentError(
1141
+ "AGENT_NOT_FOUND",
1142
+ `Agent "${name}" is not configured. Configured agents: ${available}.`
1143
+ );
1144
+ }
1145
+ const agent = createAgent(agentConfig);
1146
+ console.log(
1147
+ `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
1148
+ );
1149
+ const result = await agent.run({
1150
+ taskId: `test-${Date.now()}`,
1151
+ objective: "Reply with the single word: OK"
1152
+ });
1153
+ console.log(` status: ${result.status}`);
1154
+ if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
1155
+ if (result.summary) console.log(` output: ${result.summary}`);
1156
+ if (result.rawOutput && result.rawOutput !== result.summary) {
1157
+ console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
1158
+ }
1159
+ if (result.error) console.log(` error: ${result.error}`);
1160
+ if (result.status !== "completed") process.exitCode = 1;
813
1161
  }
814
1162
 
815
1163
  // src/cli/commands/roles.ts