@hilbras/omninode 0.3.0 → 0.5.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/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,21 @@ 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 TaskError = class extends OmniNodeError {
38
+ constructor(code, message, options = {}) {
39
+ super(code, message, options);
40
+ }
41
+ };
42
+ var ProtocolError = class extends OmniNodeError {
43
+ constructor(message, options = {}) {
44
+ super("PROTOCOL_VIOLATION", message, options);
45
+ }
46
+ };
32
47
  function isOmniNodeError(value) {
33
48
  return value instanceof OmniNodeError;
34
49
  }
@@ -55,6 +70,8 @@ var agentConfigSchema = z.object({
55
70
  args: z.array(z.string()).optional(),
56
71
  cwd: z.string().optional(),
57
72
  env: z.record(z.string()).optional(),
73
+ input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
74
+ timeout_ms: z.number().int().positive().optional(),
58
75
  metadata: z.record(z.unknown()).optional()
59
76
  }).strict();
60
77
  var roleConfigSchema = z.object({
@@ -166,6 +183,8 @@ function toAgentConfig(raw) {
166
183
  args: raw.args,
167
184
  cwd: raw.cwd,
168
185
  env: raw.env,
186
+ inputMode: raw.input_mode,
187
+ timeoutMs: raw.timeout_ms,
169
188
  metadata: raw.metadata
170
189
  };
171
190
  }
@@ -243,10 +262,6 @@ function registerProjectCommands(program) {
243
262
  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
263
  }
245
264
 
246
- // src/cli/commands/providers.ts
247
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
248
- import { parseDocument } from "yaml";
249
-
250
265
  // src/providers/auth.ts
251
266
  function resolveApiKey(config, env = process.env) {
252
267
  if (!config.apiKeyEnvVar) return void 0;
@@ -610,12 +625,36 @@ var ModelRegistry = class {
610
625
  };
611
626
 
612
627
  // src/cli/commands/shared.ts
628
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
629
+ import { parseDocument } from "yaml";
613
630
  function notImplemented(command, when) {
614
631
  return new OmniNodeError(
615
632
  "NOT_IMPLEMENTED",
616
633
  `${command} is planned for ${when}. Edit omninode.yaml directly for now.`
617
634
  );
618
635
  }
636
+ function appendToConfigList(listKey, item) {
637
+ const configPath = findConfigFile();
638
+ if (!configPath) {
639
+ throw new OmniNodeError("CONFIG_NOT_FOUND", "No omninode.yaml found. Run `omninode init` first.");
640
+ }
641
+ const doc = parseDocument(readFileSync2(configPath, "utf8"));
642
+ if (doc.getIn(["project", listKey]) === void 0) {
643
+ doc.setIn(["project", listKey], [item]);
644
+ } else {
645
+ doc.addIn(["project", listKey], item);
646
+ }
647
+ const validated = appConfigSchema.safeParse(doc.toJS());
648
+ if (!validated.success) {
649
+ const issues = validated.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
650
+ throw new ConfigError(
651
+ "CONFIG_INVALID",
652
+ `Editing ${configPath} would produce an invalid configuration: ${issues}`
653
+ );
654
+ }
655
+ writeFileSync2(configPath, doc.toString(), "utf8");
656
+ return configPath;
657
+ }
619
658
 
620
659
  // src/cli/commands/providers.ts
621
660
  function registerProviderCommands(program) {
@@ -675,25 +714,11 @@ async function addProvider(options) {
675
714
  if (existing.project.providers.some((p) => p.name === newProvider.name)) {
676
715
  throw new OmniNodeError(
677
716
  "CLI_USAGE",
678
- `Provider "${newProvider.name}" already exists in ${configPath}.`
717
+ `Provider "${newProvider.name}" already exists in ${findConfigFile() ?? "omninode.yaml"}.`
679
718
  );
680
719
  }
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}`
693
- );
694
- }
695
- writeFileSync2(configPath, doc.toString(), "utf8");
696
- console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${configPath}.`);
720
+ const writtenPath = appendToConfigList("providers", newProvider);
721
+ console.log(`Added provider "${newProvider.name}" (${newProvider.type}) to ${writtenPath}.`);
697
722
  if (newProvider.api_key_env_var) {
698
723
  console.log(
699
724
  `Set $${newProvider.api_key_env_var} in your environment, then run: omninode provider test ${newProvider.name}`
@@ -790,26 +815,381 @@ function registerModelsCommand(program) {
790
815
  });
791
816
  }
792
817
 
818
+ // src/agents/process/index.ts
819
+ import { spawn } from "child_process";
820
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
821
+ var KILL_GRACE_MS = 5e3;
822
+ function composeTaskText(input) {
823
+ const sections = [`# Objective
824
+ ${input.objective}`];
825
+ if (input.role) {
826
+ const lines = ["# Role", `${input.role.name} (${input.role.id})`];
827
+ if (input.role.responsibilities.length > 0) {
828
+ lines.push("", "Responsibilities:", ...input.role.responsibilities.map((r) => `- ${r}`));
829
+ }
830
+ if (input.role.expectedOutputs.length > 0) {
831
+ lines.push("", "Expected outputs:", ...input.role.expectedOutputs.map((o) => `- ${o}`));
832
+ }
833
+ sections.push(lines.join("\n"));
834
+ }
835
+ if (input.context) sections.push(`# Context
836
+ ${input.context}`);
837
+ if (input.instruction) sections.push(`# Instruction
838
+ ${input.instruction}`);
839
+ return sections.join("\n\n");
840
+ }
841
+ function buildProtocolMessages(input, agentName) {
842
+ const meta = {
843
+ taskId: input.taskId,
844
+ agent: agentName,
845
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
846
+ };
847
+ const messages = [
848
+ { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
849
+ ];
850
+ if (input.role) messages.push({ type: "ROLE", payload: input.role });
851
+ if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
852
+ if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
853
+ return messages.map((message) => JSON.stringify({ meta, message }));
854
+ }
855
+ var ProcessAgent = class {
856
+ config;
857
+ info;
858
+ log;
859
+ running = /* @__PURE__ */ new Map();
860
+ constructor(config, log = logger) {
861
+ if (!config.command) {
862
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${config.name}" has no command configured.`);
863
+ }
864
+ this.config = config;
865
+ this.log = log.child({ agent: config.name });
866
+ this.info = {
867
+ name: config.name,
868
+ integration: "process",
869
+ command: config.command,
870
+ cwd: config.cwd,
871
+ status: "ready",
872
+ capabilities: [config.inputMode ?? "stdin"]
873
+ };
874
+ }
875
+ async run(input) {
876
+ const command = this.config.command;
877
+ if (!command) {
878
+ throw new AgentError("AGENT_NOT_FOUND", `Agent "${this.config.name}" has no command configured.`);
879
+ }
880
+ const mode = this.config.inputMode ?? "stdin";
881
+ const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
882
+ return new Promise((resolve, reject) => {
883
+ const child = spawn(
884
+ command,
885
+ [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
886
+ {
887
+ cwd: this.config.cwd,
888
+ env: { ...process.env, ...this.config.env },
889
+ stdio: ["pipe", "pipe", "pipe"]
890
+ }
891
+ );
892
+ this.running.set(input.taskId, child);
893
+ let stdout = "";
894
+ let stderr = "";
895
+ let timedOut = false;
896
+ let settled = false;
897
+ const timer = setTimeout(() => {
898
+ timedOut = true;
899
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
900
+ child.kill("SIGTERM");
901
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
902
+ }, timeoutMs);
903
+ const settle = (finish) => {
904
+ if (settled) return;
905
+ settled = true;
906
+ clearTimeout(timer);
907
+ this.running.delete(input.taskId);
908
+ finish();
909
+ };
910
+ child.stdout?.on("data", (chunk) => {
911
+ stdout += chunk.toString();
912
+ });
913
+ child.stderr?.on("data", (chunk) => {
914
+ stderr += chunk.toString();
915
+ });
916
+ child.stdin?.on("error", () => {
917
+ });
918
+ if (mode !== "arg") {
919
+ const payload = mode === "protocol" ? buildProtocolMessages(input, this.config.name).join("\n") + "\n" : composeTaskText(input) + "\n";
920
+ child.stdin?.write(payload);
921
+ child.stdin?.end();
922
+ }
923
+ child.on("error", (error) => {
924
+ settle(() => {
925
+ if (error.code === "ENOENT") {
926
+ reject(
927
+ new AgentError(
928
+ "AGENT_NOT_FOUND",
929
+ `Agent "${this.config.name}": command "${command}" was not found. Is it installed and on PATH?`,
930
+ { cause: error }
931
+ )
932
+ );
933
+ return;
934
+ }
935
+ reject(
936
+ new AgentError("AGENT_FAILED", `Agent "${this.config.name}" failed to start: ${error.message}.`, {
937
+ cause: error
938
+ })
939
+ );
940
+ });
941
+ });
942
+ child.on("close", (code, signal) => {
943
+ settle(() => {
944
+ if (timedOut) {
945
+ reject(
946
+ new AgentError(
947
+ "AGENT_TIMEOUT",
948
+ `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
949
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
950
+ )
951
+ );
952
+ return;
953
+ }
954
+ const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
955
+ if (mode === "protocol") {
956
+ try {
957
+ resolve(
958
+ interpretProtocolRun({
959
+ input,
960
+ agentName: this.config.name,
961
+ stdout,
962
+ exitCode: code,
963
+ logs
964
+ })
965
+ );
966
+ } catch (error) {
967
+ reject(error);
968
+ }
969
+ return;
970
+ }
971
+ const trimmed = stdout.trim();
972
+ resolve({
973
+ taskId: input.taskId,
974
+ status: code === 0 ? "completed" : "failed",
975
+ summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
976
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
977
+ exitCode: code ?? void 0,
978
+ logs,
979
+ error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
980
+ });
981
+ });
982
+ });
983
+ });
984
+ }
985
+ /** Ask the agent to stop current work (SIGTERM). */
986
+ async cancel(taskId) {
987
+ const child = this.running.get(taskId);
988
+ if (child) child.kill("SIGTERM");
989
+ }
990
+ runningTaskIds() {
991
+ return [...this.running.keys()];
992
+ }
993
+ };
994
+ function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
995
+ const reports = [];
996
+ let summary;
997
+ let error;
998
+ let sawProtocolMessage = false;
999
+ for (const line of stdout.split("\n")) {
1000
+ const trimmed2 = line.trim();
1001
+ if (trimmed2.length === 0) continue;
1002
+ const message = parseAgentMessage(trimmed2);
1003
+ if (!message) continue;
1004
+ sawProtocolMessage = true;
1005
+ if (message.type === "REPORT") {
1006
+ const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
1007
+ if (report) reports.push(report);
1008
+ } else if (message.type === "COMPLETION") {
1009
+ const payload = message.payload;
1010
+ if (typeof payload?.summary === "string") summary = payload.summary;
1011
+ } else if (message.type === "ERROR") {
1012
+ const payload = message.payload;
1013
+ const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
1014
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1015
+ error = `Agent error${code}: ${detail}`;
1016
+ }
1017
+ }
1018
+ if (!sawProtocolMessage && exitCode === 0) {
1019
+ throw new ProtocolError(
1020
+ `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
1021
+ { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
1022
+ );
1023
+ }
1024
+ const trimmed = stdout.trim();
1025
+ return {
1026
+ taskId: input.taskId,
1027
+ status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
1028
+ summary: summary ?? reports[0]?.summary,
1029
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1030
+ exitCode: exitCode ?? void 0,
1031
+ logs,
1032
+ reports: reports.length > 0 ? reports : void 0,
1033
+ error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1034
+ };
1035
+ }
1036
+ function parseAgentMessage(line) {
1037
+ try {
1038
+ const parsed = JSON.parse(line);
1039
+ if (typeof parsed.type === "string") {
1040
+ return { type: parsed.type, payload: parsed.payload };
1041
+ }
1042
+ const inner = parsed.message;
1043
+ if (inner && typeof inner.type === "string") {
1044
+ return { type: inner.type, payload: inner.payload };
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ return void 0;
1049
+ }
1050
+ function normalizeReport(payload, taskId, agentName, index) {
1051
+ if (payload === null || typeof payload !== "object") return void 0;
1052
+ const raw = payload;
1053
+ if (typeof raw.summary !== "string") return void 0;
1054
+ return {
1055
+ id: typeof raw.id === "string" ? raw.id : `${taskId}-report-${index}`,
1056
+ taskId: typeof raw.taskId === "string" ? raw.taskId : taskId,
1057
+ agent: typeof raw.agent === "string" ? raw.agent : agentName,
1058
+ summary: raw.summary,
1059
+ findings: Array.isArray(raw.findings) ? raw.findings : [],
1060
+ recommendations: Array.isArray(raw.recommendations) ? raw.recommendations : [],
1061
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString()
1062
+ };
1063
+ }
1064
+ function firstLine(text, maxLength = 200) {
1065
+ const line = text.split("\n", 1)[0] ?? text;
1066
+ return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1067
+ }
1068
+
1069
+ // src/agents/registry.ts
1070
+ var AgentRegistry = class {
1071
+ agents = /* @__PURE__ */ new Map();
1072
+ register(agent) {
1073
+ this.agents.set(agent.info.name, agent);
1074
+ }
1075
+ get(name) {
1076
+ return this.agents.get(name);
1077
+ }
1078
+ list() {
1079
+ return [...this.agents.values()].sort((a, b) => a.info.name.localeCompare(b.info.name));
1080
+ }
1081
+ };
1082
+
1083
+ // src/agents/factory.ts
1084
+ function createAgent(config) {
1085
+ if (config.integration === "process") {
1086
+ return new ProcessAgent(config);
1087
+ }
1088
+ if (config.integration === "native") {
1089
+ throw new OmniNodeError(
1090
+ "NOT_IMPLEMENTED",
1091
+ `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1092
+ );
1093
+ }
1094
+ throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
1095
+ }
1096
+ function buildAgentRegistry(config, log = logger) {
1097
+ const registry = new AgentRegistry();
1098
+ for (const agentConfig of config.project.agents) {
1099
+ try {
1100
+ registry.register(createAgent(agentConfig));
1101
+ } catch (error) {
1102
+ log.warn(
1103
+ `Skipping agent "${agentConfig.name}": ${error instanceof Error ? error.message : String(error)}`
1104
+ );
1105
+ }
1106
+ }
1107
+ return registry;
1108
+ }
1109
+
793
1110
  // src/cli/commands/agents.ts
1111
+ var collectArgs = (value, previous) => [...previous, value];
794
1112
  function registerAgentCommands(program) {
795
1113
  const agent = program.command("agent").description("Manage CLI agents registered with OmniNode.");
796
1114
  agent.command("list").description("List configured agents.").action(async () => {
797
1115
  const config = loadConfig();
798
1116
  const agents = config.project.agents;
799
1117
  if (agents.length === 0) {
800
- console.log("No agents configured. Add one under `project.agents` in omninode.yaml.");
1118
+ console.log("No agents configured. Add one with `omninode agent add` or edit omninode.yaml.");
801
1119
  return;
802
1120
  }
803
1121
  for (const agent2 of agents) {
804
1122
  const command = agent2.command ?? "(no command)";
805
1123
  console.log(
806
- `${agent2.name} integration=${agent2.integration} command=${command}`
1124
+ `${agent2.name} integration=${agent2.integration} command=${command} input=${agent2.inputMode ?? "stdin"}`
807
1125
  );
808
1126
  }
809
1127
  });
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)");
1128
+ agent.command("add").description(
1129
+ "Register a CLI agent in omninode.yaml. Agents run as child processes (stdin/stdout); native adapters come in a later phase."
1130
+ ).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(
1131
+ "--input-mode <mode>",
1132
+ "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
1133
+ "stdin"
1134
+ ).option(
1135
+ "--timeout-ms <ms>",
1136
+ "Per-task timeout in milliseconds.",
1137
+ (value) => Number(value)
1138
+ ).action(async (options) => addAgent(options));
1139
+ agent.command("test <name>").description("Verify the OmniNode \u2194 agent connection by sending a trivial task.").action(async (name) => testAgent(name));
1140
+ }
1141
+ async function addAgent(options) {
1142
+ const candidate = agentConfigSchema.safeParse({
1143
+ name: options.name,
1144
+ type: "cli",
1145
+ command: options.command,
1146
+ ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
1147
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1148
+ input_mode: options.inputMode,
1149
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
1150
+ });
1151
+ if (!candidate.success) {
1152
+ const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
1153
+ throw new OmniNodeError("CLI_USAGE", `Invalid agent options: ${issues}`);
1154
+ }
1155
+ const newAgent = candidate.data;
1156
+ const existing = loadConfig();
1157
+ if (existing.project.agents.some((a) => a.name === newAgent.name)) {
1158
+ throw new OmniNodeError(
1159
+ "CLI_USAGE",
1160
+ `Agent "${newAgent.name}" already exists in omninode.yaml. Remove it first or pick another name.`
1161
+ );
1162
+ }
1163
+ const configPath = appendToConfigList("agents", newAgent);
1164
+ console.log(`Added agent "${newAgent.name}" (${newAgent.type}, input=${newAgent.input_mode}) to ${configPath}.`);
1165
+ console.log(`Run \`omninode agent test ${newAgent.name}\` to verify the connection.`);
1166
+ }
1167
+ async function testAgent(name) {
1168
+ const config = loadConfig();
1169
+ const agentConfig = config.project.agents.find((a) => a.name === name);
1170
+ if (!agentConfig) {
1171
+ const available = config.project.agents.map((a) => a.name).join(", ") || "(none)";
1172
+ throw new AgentError(
1173
+ "AGENT_NOT_FOUND",
1174
+ `Agent "${name}" is not configured. Configured agents: ${available}.`
1175
+ );
1176
+ }
1177
+ const agent = createAgent(agentConfig);
1178
+ console.log(
1179
+ `Testing agent "${name}" (${agent.info.integration}, input=${agentConfig.inputMode ?? "stdin"}) \u2014 sending a trivial task.`
1180
+ );
1181
+ const result = await agent.run({
1182
+ taskId: `test-${Date.now()}`,
1183
+ objective: "Reply with the single word: OK"
812
1184
  });
1185
+ console.log(` status: ${result.status}`);
1186
+ if (result.exitCode !== void 0) console.log(` exit: ${result.exitCode}`);
1187
+ if (result.summary) console.log(` output: ${result.summary}`);
1188
+ if (result.rawOutput && result.rawOutput !== result.summary) {
1189
+ console.log(` raw: ${result.rawOutput.slice(0, 200).replace(/\n/g, " ")}`);
1190
+ }
1191
+ if (result.error) console.log(` error: ${result.error}`);
1192
+ if (result.status !== "completed") process.exitCode = 1;
813
1193
  }
814
1194
 
815
1195
  // src/cli/commands/roles.ts
@@ -831,19 +1211,305 @@ function registerRoleCommands(program) {
831
1211
  });
832
1212
  }
833
1213
 
1214
+ // src/tasks/store.ts
1215
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
1216
+ import { dirname } from "path";
1217
+ var FileTaskStore = class {
1218
+ filePath;
1219
+ constructor(directory = `${process.cwd()}/.omninode`) {
1220
+ this.filePath = `${directory}/tasks.json`;
1221
+ }
1222
+ async save(task) {
1223
+ const tasks = await this.load();
1224
+ const index = tasks.findIndex((t) => t.id === task.id);
1225
+ if (index >= 0) tasks[index] = task;
1226
+ else tasks.push(task);
1227
+ await this.write(tasks);
1228
+ }
1229
+ async get(id) {
1230
+ const tasks = await this.load();
1231
+ return tasks.find((t) => t.id === id);
1232
+ }
1233
+ async list(filter = {}) {
1234
+ const tasks = await this.load();
1235
+ return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1236
+ }
1237
+ async load() {
1238
+ try {
1239
+ const raw = await readFile(this.filePath, "utf8");
1240
+ const parsed = JSON.parse(raw);
1241
+ return Array.isArray(parsed) ? parsed : [];
1242
+ } catch {
1243
+ return [];
1244
+ }
1245
+ }
1246
+ async write(tasks) {
1247
+ const tmp = `${this.filePath}.tmp`;
1248
+ await mkdir(dirname(this.filePath), { recursive: true });
1249
+ await writeFile(tmp, `${JSON.stringify(tasks, null, 2)}
1250
+ `, "utf8");
1251
+ await rename(tmp, this.filePath);
1252
+ }
1253
+ };
1254
+
1255
+ // src/tasks/engine.ts
1256
+ import { randomBytes } from "crypto";
1257
+ var ALLOWED_TRANSITIONS = {
1258
+ created: ["queued", "running", "cancelled"],
1259
+ queued: ["running", "cancelled"],
1260
+ running: ["completed", "failed", "cancelled"],
1261
+ waiting: ["running", "cancelled"],
1262
+ completed: [],
1263
+ failed: [],
1264
+ cancelled: []
1265
+ };
1266
+ var TaskEngine = class {
1267
+ constructor(options) {
1268
+ this.options = options;
1269
+ this.log = (options.log ?? logger).child({ component: "tasks" });
1270
+ }
1271
+ options;
1272
+ log;
1273
+ get store() {
1274
+ return this.options.store;
1275
+ }
1276
+ async create(input) {
1277
+ if (!input.objective || input.objective.trim().length === 0) {
1278
+ throw new TaskError("TASK_INVALID", "Task objective must not be empty.");
1279
+ }
1280
+ if (input.role && !this.options.roles.get(input.role)) {
1281
+ throw new TaskError(
1282
+ "TASK_INVALID",
1283
+ `Role "${input.role}" is not defined. Add it under project.roles in omninode.yaml.`
1284
+ );
1285
+ }
1286
+ if (input.agent && !this.options.agents.get(input.agent)) {
1287
+ throw new TaskError(
1288
+ "TASK_INVALID",
1289
+ `Agent "${input.agent}" is not registered. Add it with \`omninode agent add\`.`
1290
+ );
1291
+ }
1292
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1293
+ const task = {
1294
+ id: generateTaskId(),
1295
+ objective: input.objective.trim(),
1296
+ status: "created",
1297
+ createdAt: now,
1298
+ updatedAt: now,
1299
+ ...input.project !== void 0 ? { project: input.project } : {},
1300
+ ...input.role !== void 0 ? { role: input.role } : {},
1301
+ ...input.agent !== void 0 ? { agent: input.agent } : {},
1302
+ ...input.context !== void 0 ? { context: input.context } : {}
1303
+ };
1304
+ await this.options.store.save(task);
1305
+ this.log.info(`Created task ${task.id}.`);
1306
+ return task;
1307
+ }
1308
+ async queue(id) {
1309
+ const task = await this.mustGet(id);
1310
+ return this.transition(task, "queued");
1311
+ }
1312
+ /** Runs the task through its assigned agent and records the result. */
1313
+ async run(id) {
1314
+ const task = await this.mustGet(id);
1315
+ if (task.status !== "created" && task.status !== "queued") {
1316
+ throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
1317
+ }
1318
+ if (!task.agent) {
1319
+ throw new TaskError("TASK_INVALID", `Task ${id} has no agent assigned. Re-create it with --agent.`);
1320
+ }
1321
+ const agent = this.options.agents.get(task.agent);
1322
+ if (!agent) {
1323
+ throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1324
+ }
1325
+ const role = task.role ? this.options.roles.get(task.role) : void 0;
1326
+ const running = await this.transition(task, "running");
1327
+ this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1328
+ let output;
1329
+ try {
1330
+ output = await agent.run({
1331
+ taskId: running.id,
1332
+ objective: running.objective,
1333
+ ...role ? { role } : {},
1334
+ ...running.context?.background ? { context: running.context.background } : {}
1335
+ });
1336
+ } catch (error) {
1337
+ const message = error instanceof Error ? error.message : String(error);
1338
+ this.log.error(`Task ${task.id} failed: ${message}`);
1339
+ return this.transition(running, "failed", {
1340
+ result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
1341
+ });
1342
+ }
1343
+ const result = {
1344
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
1345
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1346
+ ...output.error !== void 0 ? { error: output.error } : {},
1347
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1348
+ };
1349
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
1350
+ this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1351
+ return this.transition(running, finalStatus, { result });
1352
+ }
1353
+ async cancel(id) {
1354
+ const task = await this.mustGet(id);
1355
+ if (!ALLOWED_TRANSITIONS[task.status].includes("cancelled")) {
1356
+ throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be cancelled.`);
1357
+ }
1358
+ if (task.agent) {
1359
+ const agent = this.options.agents.get(task.agent);
1360
+ await agent?.cancel?.(task.id);
1361
+ }
1362
+ return this.transition(task, "cancelled");
1363
+ }
1364
+ async get(id) {
1365
+ return this.options.store.get(id);
1366
+ }
1367
+ async list(filter) {
1368
+ return this.options.store.list(filter);
1369
+ }
1370
+ async mustGet(id) {
1371
+ const task = await this.options.store.get(id);
1372
+ if (!task) {
1373
+ throw new TaskError("TASK_NOT_FOUND", `Task "${id}" does not exist in this project's task store.`);
1374
+ }
1375
+ return task;
1376
+ }
1377
+ async transition(task, to, extra = {}) {
1378
+ const allowed = ALLOWED_TRANSITIONS[task.status] ?? [];
1379
+ if (!allowed.includes(to)) {
1380
+ throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1381
+ }
1382
+ const updated = {
1383
+ ...task,
1384
+ status: to,
1385
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1386
+ ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1387
+ };
1388
+ await this.options.store.save(updated);
1389
+ return updated;
1390
+ }
1391
+ };
1392
+ function generateTaskId() {
1393
+ return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1394
+ }
1395
+
1396
+ // src/roles/index.ts
1397
+ var RoleRegistry = class {
1398
+ roles = /* @__PURE__ */ new Map();
1399
+ register(role) {
1400
+ this.roles.set(role.id, role);
1401
+ }
1402
+ registerAll(roles) {
1403
+ for (const role of roles) this.register(role);
1404
+ }
1405
+ get(id) {
1406
+ return this.roles.get(id);
1407
+ }
1408
+ list() {
1409
+ return [...this.roles.values()].sort((a, b) => a.id.localeCompare(b.id));
1410
+ }
1411
+ };
1412
+ function buildRoleRegistry(config) {
1413
+ const registry = new RoleRegistry();
1414
+ registry.registerAll(config.project.roles);
1415
+ return registry;
1416
+ }
1417
+
1418
+ // src/tasks/engine-factory.ts
1419
+ function createTaskEngine(config, log = logger, directory) {
1420
+ return new TaskEngine({
1421
+ agents: buildAgentRegistry(config, log),
1422
+ roles: buildRoleRegistry(config),
1423
+ store: new FileTaskStore(directory),
1424
+ log
1425
+ });
1426
+ }
1427
+
834
1428
  // src/cli/commands/tasks.ts
1429
+ var collectFiles = (value, previous) => [...previous, value];
835
1430
  function registerTaskCommands(program) {
836
- const task = program.command("task").description("Create, run and inspect tasks (Phase 4+).");
837
- task.command("create").description("Create a task (planned for Phase 4 \u2014 Roles & Tasks).").action(() => {
838
- throw notImplemented("omninode task create", "Phase 4 (Roles & Tasks)");
1431
+ const task = program.command("task").description("Create, run and inspect tasks (\xA713).");
1432
+ task.command("create <objective>").description("Create a task. Assign a role and/or agent; run it with `omninode task run`.").option("-r, --role <roleId>", "Role id from project.roles (validated at creation).").option("-a, --agent <name>", "Agent name from project.agents (validated at creation).").option("-f, --file <path>", "File or glob relevant to the task (repeatable).", collectFiles).action(async (objective, options) => {
1433
+ const config = loadConfig();
1434
+ const engine = createTaskEngine(config);
1435
+ const created = await engine.create({
1436
+ objective,
1437
+ project: config.project.name,
1438
+ ...options.role !== void 0 ? { role: options.role } : {},
1439
+ ...options.agent !== void 0 ? { agent: options.agent } : {},
1440
+ ...options.file !== void 0 && options.file.length > 0 ? { context: { files: options.file } } : {}
1441
+ });
1442
+ console.log(`Created task ${created.id} (${created.status})`);
1443
+ console.log(` objective: ${created.objective}`);
1444
+ if (created.role) console.log(` role: ${created.role}`);
1445
+ if (created.agent) console.log(` agent: ${created.agent}`);
1446
+ if (created.context?.files) console.log(` files: ${created.context.files.join(", ")}`);
1447
+ console.log(`Run \`omninode task run ${created.id}\` to execute it.`);
1448
+ });
1449
+ task.command("run <task-id>").description("Run a task through its assigned agent.").action(async (taskId) => {
1450
+ const config = loadConfig();
1451
+ const engine = createTaskEngine(config);
1452
+ console.log(`Running task ${taskId}...`);
1453
+ const finished = await engine.run(taskId);
1454
+ printOutcome(finished);
1455
+ if (finished.status !== "completed") process.exitCode = 1;
1456
+ });
1457
+ task.command("status <task-id>").description("Show task details and current status.").action(async (taskId) => {
1458
+ const config = loadConfig();
1459
+ const engine = createTaskEngine(config);
1460
+ const found = await engine.get(taskId);
1461
+ if (!found) {
1462
+ throw new TaskError(
1463
+ "TASK_NOT_FOUND",
1464
+ `Task "${taskId}" does not exist in this project's task store.`
1465
+ );
1466
+ }
1467
+ printOutcome(found, { verbose: true });
839
1468
  });
840
- task.command("run <objective>").description("Run a task or pipeline (planned for Phase 5 \u2014 Pipeline Engine).").action(() => {
841
- throw notImplemented("omninode task run", "Phase 5 (Pipeline Engine)");
1469
+ task.command("list").description("List tasks in the project's task store.").option("-s, --status <status>", "Filter by status (created, queued, running, completed, failed, cancelled).").action(async (options) => {
1470
+ const config = loadConfig();
1471
+ const engine = createTaskEngine(config);
1472
+ const tasks = await engine.list(
1473
+ options.status !== void 0 ? { status: options.status } : {}
1474
+ );
1475
+ if (tasks.length === 0) {
1476
+ console.log("No tasks found. Create one with `omninode task create`.");
1477
+ return;
1478
+ }
1479
+ for (const t of tasks) {
1480
+ console.log(`${t.id} ${t.status.padEnd(10)} ${truncate(t.objective, 60)}`);
1481
+ }
842
1482
  });
843
- task.command("status <task-id>").description("Show task status (planned for Phase 4 \u2014 Roles & Tasks).").action(() => {
844
- throw notImplemented("omninode task status", "Phase 4 (Roles & Tasks)");
1483
+ task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
1484
+ const config = loadConfig();
1485
+ const engine = createTaskEngine(config);
1486
+ const cancelled = await engine.cancel(taskId);
1487
+ console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
845
1488
  });
846
1489
  }
1490
+ function printOutcome(task, options = {}) {
1491
+ console.log(` id: ${task.id}`);
1492
+ console.log(` status: ${task.status}`);
1493
+ console.log(` created: ${task.createdAt}`);
1494
+ if (options.verbose) {
1495
+ console.log(` project: ${task.project ?? "(none)"}`);
1496
+ if (task.role) console.log(` role: ${task.role}`);
1497
+ if (task.agent) console.log(` agent: ${task.agent}`);
1498
+ if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
1499
+ console.log(` objective: ${task.objective}`);
1500
+ }
1501
+ const result = task.result;
1502
+ if (!result) return;
1503
+ if (result.summary) console.log(` summary: ${result.summary}`);
1504
+ if (result.reports && result.reports.length > 0) {
1505
+ console.log(` reports: ${result.reports.length} structured report(s)`);
1506
+ }
1507
+ if (result.error) console.log(` error: ${result.error}`);
1508
+ if (result.finishedAt) console.log(` ended: ${result.finishedAt}`);
1509
+ }
1510
+ function truncate(text, max) {
1511
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1512
+ }
847
1513
 
848
1514
  // src/cli/commands/reports.ts
849
1515
  function registerReportCommands(program) {