@hilbras/omninode 0.4.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/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.4.0 — Phase 3: CLI Agent System** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
21
+ **v0.5.0 — Phase 4: Roles & Tasks** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
22
22
  for the full plan and roadmap).
23
23
 
24
24
  What exists today:
@@ -44,6 +44,15 @@ What exists today:
44
44
  `TASK`/`ROLE`/`CONTEXT`/`INSTRUCTION` envelopes in, `REPORT`/`COMPLETION`/
45
45
  `ERROR` messages out, parsed into structured reports)
46
46
  - Agent factory and registry (`createAgent`, `AgentRegistry`)
47
+ - **Roles & tasks** (`src/roles/`, `src/tasks/`):
48
+ - `RoleRegistry` with assignment validation — a task can only reference roles
49
+ and agents that actually exist
50
+ - Task lifecycle (created → queued → running → completed/failed/cancelled)
51
+ with transition enforcement
52
+ - `TaskEngine` runs tasks through their assigned agent, records structured
53
+ results (summary, reports, errors, timestamps)
54
+ - `TaskStore` interface with a local-first JSON implementation in
55
+ `.omninode/tasks.json` (atomic writes; swap in any backend)
47
56
  - Typed error hierarchy (`src/errors/`)
48
57
  - Leveled logger with pluggable sink (`src/logger/`)
49
58
  - Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
@@ -93,6 +102,15 @@ omninode agent add --name opencode --command opencode --input-mode arg
93
102
  omninode agent test opencode # sends a trivial task over the real adapter
94
103
  ```
95
104
 
105
+ Create, run and track tasks (the `Project → Task → Role → Agent` chain):
106
+
107
+ ```bash
108
+ omninode task create "audit the auth module" --role security-reviewer --agent opencode
109
+ omninode task run task-xxxxxx
110
+ omninode task status task-xxxxxx
111
+ omninode task list
112
+ ```
113
+
96
114
  See [omninode.yaml.example](omninode.yaml.example) for a full configuration.
97
115
 
98
116
  ### Configuration
@@ -125,8 +143,8 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
125
143
  | v0.1.0 | Foundation |
126
144
  | v0.2.0 | Provider System |
127
145
  | v0.3.0 | OmniHilbras Integration (optional, like every provider) |
128
- | **v0.4.0** | **CLI Agent System (current)** |
129
- | v0.5.0 | Roles & Tasks |
146
+ | v0.4.0 | Agent System |
147
+ | **v0.5.0** | **Roles & Tasks (current)** |
130
148
  | v0.6.0 | Pipeline Engine |
131
149
  | v0.7.0 | Multi-AI Reports |
132
150
  | v0.8.0 | Remembera (optional memory integration) |
package/dist/cli/index.js CHANGED
@@ -34,6 +34,11 @@ var AgentError = class extends OmniNodeError {
34
34
  super(code, message, options);
35
35
  }
36
36
  };
37
+ var TaskError = class extends OmniNodeError {
38
+ constructor(code, message, options = {}) {
39
+ super(code, message, options);
40
+ }
41
+ };
37
42
  var ProtocolError = class extends OmniNodeError {
38
43
  constructor(message, options = {}) {
39
44
  super("PROTOCOL_VIOLATION", message, options);
@@ -1061,6 +1066,20 @@ function firstLine(text, maxLength = 200) {
1061
1066
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1062
1067
  }
1063
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
+
1064
1083
  // src/agents/factory.ts
1065
1084
  function createAgent(config) {
1066
1085
  if (config.integration === "process") {
@@ -1074,6 +1093,19 @@ function createAgent(config) {
1074
1093
  }
1075
1094
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
1076
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
+ }
1077
1109
 
1078
1110
  // src/cli/commands/agents.ts
1079
1111
  var collectArgs = (value, previous) => [...previous, value];
@@ -1179,19 +1211,305 @@ function registerRoleCommands(program) {
1179
1211
  });
1180
1212
  }
1181
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
+
1182
1428
  // src/cli/commands/tasks.ts
1429
+ var collectFiles = (value, previous) => [...previous, value];
1183
1430
  function registerTaskCommands(program) {
1184
- const task = program.command("task").description("Create, run and inspect tasks (Phase 4+).");
1185
- task.command("create").description("Create a task (planned for Phase 4 \u2014 Roles & Tasks).").action(() => {
1186
- 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 });
1187
1468
  });
1188
- task.command("run <objective>").description("Run a task or pipeline (planned for Phase 5 \u2014 Pipeline Engine).").action(() => {
1189
- 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
+ }
1190
1482
  });
1191
- task.command("status <task-id>").description("Show task status (planned for Phase 4 \u2014 Roles & Tasks).").action(() => {
1192
- 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}".`);
1193
1488
  });
1194
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
+ }
1195
1513
 
1196
1514
  // src/cli/commands/reports.ts
1197
1515
  function registerReportCommands(program) {