@hilbras/omninode 0.8.0 → 0.9.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 +28 -3
- package/dist/cli/index.js +429 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +148 -5
- package/dist/index.js +398 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
21
|
+
**v0.9.0 — Phase 8: Planner 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:
|
|
@@ -84,6 +84,19 @@ What exists today:
|
|
|
84
84
|
HTTP (base URL + `api_key_env_var`); strictly optional, like every
|
|
85
85
|
Hilbras component
|
|
86
86
|
- Memory failures are best-effort: logged, never fatal to a task
|
|
87
|
+
- **Planner layer** (`src/planner/`):
|
|
88
|
+
- Typed plan schema (`Plan` with ordered steps, targets, acceptance
|
|
89
|
+
criteria, risks, and finding traceability via `sourceFindings`)
|
|
90
|
+
- Planner context builder: objective + role + combined findings with
|
|
91
|
+
sources + relevant memory + research context
|
|
92
|
+
- `ModelPlanner` — asks a chat model (`provider:model-id`) for JSON matching
|
|
93
|
+
the plan contract; unparseable output degrades gracefully to the heuristic
|
|
94
|
+
fallback
|
|
95
|
+
- `HeuristicPlanner` — deterministic, offline plan from recommendations and
|
|
96
|
+
significant findings (also the fallback)
|
|
97
|
+
- Plans persist to `.omninode/plans.json`; pipeline runs record `planId`
|
|
98
|
+
- Configure with `planner: { kind: model | heuristic, model, instruction }`
|
|
99
|
+
— never hardcoded to any provider
|
|
87
100
|
- Typed error hierarchy (`src/errors/`)
|
|
88
101
|
- Leveled logger with pluggable sink (`src/logger/`)
|
|
89
102
|
- Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
|
|
@@ -164,6 +177,17 @@ project:
|
|
|
164
177
|
omninode pipeline run repo-audit "Audit the authentication module"
|
|
165
178
|
omninode report combined # the aggregated intelligence of that run
|
|
166
179
|
omninode report show combined-xxx # grouped findings with per-source attribution
|
|
180
|
+
omninode plan show plan-xxx # the implementation plan the planner produced
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The planner layer (§18) turns combined intelligence into an ordered,
|
|
184
|
+
actionable implementation plan. Configure it once:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
project:
|
|
188
|
+
planner:
|
|
189
|
+
kind: model # or heuristic for offline planning
|
|
190
|
+
model: my-gateway:chatgpt
|
|
167
191
|
```
|
|
168
192
|
|
|
169
193
|
Persistent memory (§19–§20) — opt in with `memory: { provider: local }` or
|
|
@@ -210,8 +234,9 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
|
|
|
210
234
|
| v0.4.0 | Agent System |
|
|
211
235
|
| v0.5.0 | Roles & Tasks |
|
|
212
236
|
| v0.7.0 | Multi-AI Reports |
|
|
213
|
-
|
|
|
214
|
-
| v0.
|
|
237
|
+
| v0.7.0 | Multi-AI Reports |
|
|
238
|
+
| v0.8.0 | Remembera / Memory |
|
|
239
|
+
| **v0.9.0** | **Planner (current)** |
|
|
215
240
|
| v0.10.0 | End-to-End Runtime |
|
|
216
241
|
| v1.0.0 | Production Release |
|
|
217
242
|
|
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.
|
|
9
|
+
var OMNINODE_VERSION = "0.9.0";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -49,6 +49,11 @@ var ProtocolError = class extends OmniNodeError {
|
|
|
49
49
|
super("PROTOCOL_VIOLATION", message, options);
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
var PlannerError = class extends OmniNodeError {
|
|
53
|
+
constructor(code, message, options = {}) {
|
|
54
|
+
super(code, message, options);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
52
57
|
var MemoryError = class extends OmniNodeError {
|
|
53
58
|
constructor(message, options = {}) {
|
|
54
59
|
super("MEMORY_UNAVAILABLE", message, options);
|
|
@@ -111,12 +116,18 @@ var pipelineConfigSchema = z.object({
|
|
|
111
116
|
role: z.string().optional(),
|
|
112
117
|
steps: z.array(pipelineStepConfigSchema).min(1)
|
|
113
118
|
}).strict();
|
|
119
|
+
var plannerConfigSchema = z.object({
|
|
120
|
+
kind: z.enum(["model", "heuristic"]).default("model"),
|
|
121
|
+
model: z.string().optional(),
|
|
122
|
+
instruction: z.string().optional()
|
|
123
|
+
}).strict();
|
|
114
124
|
var projectConfigSchema = z.object({
|
|
115
125
|
name: z.string().min(1),
|
|
116
126
|
providers: z.array(providerConfigSchema).default([]),
|
|
117
127
|
agents: z.array(agentConfigSchema).default([]),
|
|
118
128
|
roles: z.array(roleConfigSchema).default([]),
|
|
119
129
|
pipelines: z.array(pipelineConfigSchema).default([]),
|
|
130
|
+
planner: plannerConfigSchema.optional(),
|
|
120
131
|
memory: z.object({
|
|
121
132
|
provider: z.string().min(1).default("local"),
|
|
122
133
|
base_url: z.string().url().optional(),
|
|
@@ -191,6 +202,13 @@ function toAppConfig(yaml) {
|
|
|
191
202
|
agents: yaml.project.agents.map(toAgentConfig),
|
|
192
203
|
roles: yaml.project.roles.map(toRoleDefinition),
|
|
193
204
|
pipelines: yaml.project.pipelines.map(toPipelineDefinition),
|
|
205
|
+
...yaml.project.planner !== void 0 ? {
|
|
206
|
+
planner: {
|
|
207
|
+
kind: yaml.project.planner.kind,
|
|
208
|
+
model: yaml.project.planner.model,
|
|
209
|
+
instruction: yaml.project.planner.instruction
|
|
210
|
+
}
|
|
211
|
+
} : {},
|
|
194
212
|
...yaml.project.memory !== void 0 ? {
|
|
195
213
|
memory: {
|
|
196
214
|
provider: yaml.project.memory.provider,
|
|
@@ -2282,7 +2300,231 @@ function registerReportCommands(program) {
|
|
|
2282
2300
|
}
|
|
2283
2301
|
|
|
2284
2302
|
// src/pipelines/engine.ts
|
|
2303
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2304
|
+
|
|
2305
|
+
// src/planner/heuristic.ts
|
|
2285
2306
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
2307
|
+
var SEVERITY_RANK2 = {
|
|
2308
|
+
info: 0,
|
|
2309
|
+
low: 1,
|
|
2310
|
+
medium: 2,
|
|
2311
|
+
high: 3,
|
|
2312
|
+
critical: 4
|
|
2313
|
+
};
|
|
2314
|
+
var HeuristicPlanner = class {
|
|
2315
|
+
name = "heuristic";
|
|
2316
|
+
async plan(request) {
|
|
2317
|
+
const steps = [];
|
|
2318
|
+
const findings = request.reports.flatMap(
|
|
2319
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
2320
|
+
);
|
|
2321
|
+
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
2322
|
+
for (const [index, text] of recommendations.entries()) {
|
|
2323
|
+
steps.push({
|
|
2324
|
+
id: `step-${steps.length + 1}`,
|
|
2325
|
+
title: truncate5(text, 120),
|
|
2326
|
+
order: steps.length + 1,
|
|
2327
|
+
...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
const significant = findings.filter(({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false).sort((a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]);
|
|
2331
|
+
for (const { finding } of significant) {
|
|
2332
|
+
steps.push(remediationStep(finding, steps.length + 1));
|
|
2333
|
+
}
|
|
2334
|
+
if (steps.length === 0) {
|
|
2335
|
+
steps.push({
|
|
2336
|
+
id: "step-1",
|
|
2337
|
+
title: "Gather additional information",
|
|
2338
|
+
description: "The research reports contained no actionable recommendations or significant findings. Investigate the objective further before planning implementation work.",
|
|
2339
|
+
order: 1
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
2343
|
+
return {
|
|
2344
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
2345
|
+
objective: request.objective,
|
|
2346
|
+
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
2347
|
+
steps,
|
|
2348
|
+
...risks.length > 0 ? { risks } : {},
|
|
2349
|
+
generatedBy: "heuristic",
|
|
2350
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2351
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2352
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
function remediationStep(finding, order) {
|
|
2357
|
+
return {
|
|
2358
|
+
id: `step-${order}`,
|
|
2359
|
+
title: `Fix: ${truncate5(finding.title, 100)}`,
|
|
2360
|
+
description: finding.detail,
|
|
2361
|
+
order,
|
|
2362
|
+
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
2363
|
+
sourceFindings: [finding.id]
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
function dedupe(texts) {
|
|
2367
|
+
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
2368
|
+
}
|
|
2369
|
+
function truncate5(text, max) {
|
|
2370
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// src/planner/model.ts
|
|
2374
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2375
|
+
import { z as z2 } from "zod";
|
|
2376
|
+
|
|
2377
|
+
// src/planner/context.ts
|
|
2378
|
+
function buildPlannerContext(request) {
|
|
2379
|
+
const parts = [`# Objective
|
|
2380
|
+
${request.objective}`];
|
|
2381
|
+
if (request.role) {
|
|
2382
|
+
const roleLines = [`# Role
|
|
2383
|
+
${request.role.name} (${request.role.id})`];
|
|
2384
|
+
if (request.role.responsibilities.length > 0) {
|
|
2385
|
+
roleLines.push("Responsibilities:", ...request.role.responsibilities.map((r) => `- ${r}`));
|
|
2386
|
+
}
|
|
2387
|
+
if (request.role.expectedOutputs.length > 0) {
|
|
2388
|
+
roleLines.push("Expected outputs:", ...request.role.expectedOutputs.map((o) => `- ${o}`));
|
|
2389
|
+
}
|
|
2390
|
+
parts.push(roleLines.join("\n"));
|
|
2391
|
+
}
|
|
2392
|
+
if (request.reports.length > 0) {
|
|
2393
|
+
const agents = [...new Set(request.reports.map((r) => r.agent))].join(", ");
|
|
2394
|
+
const findingLines = request.reports.flatMap(
|
|
2395
|
+
(report) => report.findings.map(
|
|
2396
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (source: ${report.agent}, finding: ${finding.id})` + (finding.detail ? ` \u2014 ${finding.detail}` : "")
|
|
2397
|
+
)
|
|
2398
|
+
);
|
|
2399
|
+
parts.push(
|
|
2400
|
+
`# Findings (${request.reports.length} report(s) by ${agents})
|
|
2401
|
+
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
2402
|
+
);
|
|
2403
|
+
const recommendationLines = request.reports.flatMap(
|
|
2404
|
+
(report) => report.recommendations.map((text) => `- ${text} (source: ${report.agent})`)
|
|
2405
|
+
);
|
|
2406
|
+
if (recommendationLines.length > 0) {
|
|
2407
|
+
parts.push(`# Recommendations
|
|
2408
|
+
${recommendationLines.join("\n")}`);
|
|
2409
|
+
}
|
|
2410
|
+
const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
|
|
2411
|
+
parts.push(`# Report summaries
|
|
2412
|
+
${summaryLines.join("\n")}`);
|
|
2413
|
+
}
|
|
2414
|
+
if (request.memory && request.memory.length > 0) {
|
|
2415
|
+
parts.push(
|
|
2416
|
+
`# Relevant memory
|
|
2417
|
+
` + request.memory.map((entry2) => `- [${entry2.scope}] ${entry2.content}`).join("\n")
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
if (request.context && request.context.length > 0) {
|
|
2421
|
+
parts.push(`# Research context
|
|
2422
|
+
${request.context}`);
|
|
2423
|
+
}
|
|
2424
|
+
return parts.join("\n\n");
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/planner/model.ts
|
|
2428
|
+
var planStepJsonSchema = z2.object({
|
|
2429
|
+
id: z2.string().optional(),
|
|
2430
|
+
title: z2.string().min(1),
|
|
2431
|
+
description: z2.string().optional(),
|
|
2432
|
+
order: z2.number().int().optional(),
|
|
2433
|
+
targets: z2.array(z2.string()).optional(),
|
|
2434
|
+
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
2435
|
+
depends_on: z2.array(z2.string()).optional(),
|
|
2436
|
+
source_findings: z2.array(z2.string()).optional()
|
|
2437
|
+
}).strict();
|
|
2438
|
+
var planJsonSchema = z2.object({
|
|
2439
|
+
summary: z2.string().min(1),
|
|
2440
|
+
steps: z2.array(planStepJsonSchema).min(1),
|
|
2441
|
+
risks: z2.array(z2.string()).optional(),
|
|
2442
|
+
notes: z2.array(z2.string()).optional()
|
|
2443
|
+
}).strict();
|
|
2444
|
+
var ModelPlanner = class {
|
|
2445
|
+
constructor(options) {
|
|
2446
|
+
this.options = options;
|
|
2447
|
+
this.log = (options.log ?? logger).child({ component: "planner", model: options.model });
|
|
2448
|
+
}
|
|
2449
|
+
options;
|
|
2450
|
+
name = "model";
|
|
2451
|
+
log;
|
|
2452
|
+
async plan(request) {
|
|
2453
|
+
const context = buildPlannerContext(request);
|
|
2454
|
+
const messages = [
|
|
2455
|
+
{
|
|
2456
|
+
role: "system",
|
|
2457
|
+
content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
|
|
2458
|
+
},
|
|
2459
|
+
{ role: "user", content: context }
|
|
2460
|
+
];
|
|
2461
|
+
let responseText;
|
|
2462
|
+
try {
|
|
2463
|
+
responseText = await this.options.chat(this.options.model, messages);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
2466
|
+
}
|
|
2467
|
+
const parsed = parsePlanJson(responseText);
|
|
2468
|
+
if (!parsed) {
|
|
2469
|
+
return this.degrade(request, "planner model output was not valid plan JSON");
|
|
2470
|
+
}
|
|
2471
|
+
const plan = {
|
|
2472
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
2473
|
+
objective: request.objective,
|
|
2474
|
+
summary: parsed.summary,
|
|
2475
|
+
steps: parsed.steps,
|
|
2476
|
+
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
2477
|
+
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
2478
|
+
generatedBy: `model:${this.options.model}`,
|
|
2479
|
+
model: this.options.model,
|
|
2480
|
+
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2481
|
+
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2482
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2483
|
+
};
|
|
2484
|
+
return plan;
|
|
2485
|
+
}
|
|
2486
|
+
async degrade(request, reason) {
|
|
2487
|
+
if (this.options.fallback) {
|
|
2488
|
+
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
2489
|
+
return this.options.fallback.plan(request);
|
|
2490
|
+
}
|
|
2491
|
+
throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
function parsePlanJson(text) {
|
|
2495
|
+
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
2496
|
+
const start = unfenced.indexOf("{");
|
|
2497
|
+
const end = unfenced.lastIndexOf("}");
|
|
2498
|
+
if (start < 0 || end <= start) return void 0;
|
|
2499
|
+
try {
|
|
2500
|
+
const json = JSON.parse(unfenced.slice(start, end + 1));
|
|
2501
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
2502
|
+
if (!parsed.success) return void 0;
|
|
2503
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
2504
|
+
id: step.id ?? `step-${index + 1}`,
|
|
2505
|
+
title: step.title,
|
|
2506
|
+
description: step.description,
|
|
2507
|
+
order: step.order ?? index + 1,
|
|
2508
|
+
targets: step.targets,
|
|
2509
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
2510
|
+
dependsOn: step.depends_on,
|
|
2511
|
+
sourceFindings: step.source_findings
|
|
2512
|
+
}));
|
|
2513
|
+
return {
|
|
2514
|
+
summary: parsed.data.summary,
|
|
2515
|
+
steps: steps.sort((a, b) => a.order - b.order),
|
|
2516
|
+
risks: parsed.data.risks,
|
|
2517
|
+
notes: parsed.data.notes
|
|
2518
|
+
};
|
|
2519
|
+
} catch {
|
|
2520
|
+
return void 0;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function describe(error) {
|
|
2524
|
+
return error instanceof Error ? error.message : String(error);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/pipelines/engine.ts
|
|
2286
2528
|
var PipelineEngine = class {
|
|
2287
2529
|
constructor(options) {
|
|
2288
2530
|
this.options = options;
|
|
@@ -2395,7 +2637,7 @@ var PipelineEngine = class {
|
|
|
2395
2637
|
}
|
|
2396
2638
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2397
2639
|
const run2 = {
|
|
2398
|
-
id: `run-${Date.now().toString(36)}-${
|
|
2640
|
+
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2399
2641
|
pipelineId: def.id,
|
|
2400
2642
|
status: "running",
|
|
2401
2643
|
startedAt: now,
|
|
@@ -2423,7 +2665,9 @@ var PipelineEngine = class {
|
|
|
2423
2665
|
const ctx = {
|
|
2424
2666
|
objective,
|
|
2425
2667
|
...role ? { role } : {},
|
|
2426
|
-
combinedContext: contextParts.join("\n\n")
|
|
2668
|
+
combinedContext: contextParts.join("\n\n"),
|
|
2669
|
+
reports: [...allReports],
|
|
2670
|
+
pipelineRunId: run2.id
|
|
2427
2671
|
};
|
|
2428
2672
|
return this.executeStep(step, ctx);
|
|
2429
2673
|
})
|
|
@@ -2442,6 +2686,7 @@ ${contribution}`);
|
|
|
2442
2686
|
stepId: step.id,
|
|
2443
2687
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
|
|
2444
2688
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2689
|
+
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2445
2690
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
2446
2691
|
};
|
|
2447
2692
|
run2.stepRuns.push(stepRun);
|
|
@@ -2452,6 +2697,7 @@ ${contribution}`);
|
|
|
2452
2697
|
}
|
|
2453
2698
|
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
2454
2699
|
const finalStatus = failed ? "failed" : "completed";
|
|
2700
|
+
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2455
2701
|
let combinedReportId;
|
|
2456
2702
|
if (this.options.reports) {
|
|
2457
2703
|
const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
|
|
@@ -2473,7 +2719,8 @@ ${contribution}`);
|
|
|
2473
2719
|
status: finalStatus,
|
|
2474
2720
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2475
2721
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2476
|
-
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
2722
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2723
|
+
...planId !== void 0 ? { planId } : {}
|
|
2477
2724
|
};
|
|
2478
2725
|
await this.options.store.save(finished);
|
|
2479
2726
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
@@ -2583,33 +2830,34 @@ ${contribution}`);
|
|
|
2583
2830
|
};
|
|
2584
2831
|
}
|
|
2585
2832
|
async runPlan(step, ctx) {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
{
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
try {
|
|
2605
|
-
const planText = await this.getChat()(step.model, messages);
|
|
2833
|
+
const request = {
|
|
2834
|
+
objective: ctx.objective,
|
|
2835
|
+
reports: ctx.reports,
|
|
2836
|
+
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2837
|
+
...ctx.role ? { role: ctx.role } : {},
|
|
2838
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2839
|
+
};
|
|
2840
|
+
let planner;
|
|
2841
|
+
if (step.model) {
|
|
2842
|
+
planner = new ModelPlanner({
|
|
2843
|
+
model: step.model,
|
|
2844
|
+
chat: this.getChat(),
|
|
2845
|
+
fallback: new HeuristicPlanner(),
|
|
2846
|
+
log: this.log
|
|
2847
|
+
});
|
|
2848
|
+
} else if (this.options.planner) {
|
|
2849
|
+
planner = this.options.planner;
|
|
2850
|
+
} else {
|
|
2606
2851
|
return {
|
|
2607
2852
|
status: "completed",
|
|
2608
|
-
summary:
|
|
2609
|
-
contextContribution: planText,
|
|
2853
|
+
summary: "(plan: no model or planner configured \u2014 skipped)",
|
|
2610
2854
|
taskIds: [],
|
|
2611
2855
|
reports: []
|
|
2612
2856
|
};
|
|
2857
|
+
}
|
|
2858
|
+
let plan;
|
|
2859
|
+
try {
|
|
2860
|
+
plan = await planner.plan(request);
|
|
2613
2861
|
} catch (error) {
|
|
2614
2862
|
return {
|
|
2615
2863
|
status: "failed",
|
|
@@ -2619,6 +2867,15 @@ Produce the final plan.`
|
|
|
2619
2867
|
reports: []
|
|
2620
2868
|
};
|
|
2621
2869
|
}
|
|
2870
|
+
await this.options.plans?.save(plan);
|
|
2871
|
+
return {
|
|
2872
|
+
status: "completed",
|
|
2873
|
+
summary: plan.summary,
|
|
2874
|
+
contextContribution: formatPlan(plan),
|
|
2875
|
+
taskIds: [],
|
|
2876
|
+
planId: plan.id,
|
|
2877
|
+
reports: []
|
|
2878
|
+
};
|
|
2622
2879
|
}
|
|
2623
2880
|
getChat() {
|
|
2624
2881
|
if (this.options.chat) return this.options.chat;
|
|
@@ -2628,6 +2885,21 @@ Produce the final plan.`
|
|
|
2628
2885
|
);
|
|
2629
2886
|
}
|
|
2630
2887
|
};
|
|
2888
|
+
function formatPlan(plan) {
|
|
2889
|
+
const lines = [
|
|
2890
|
+
`# Implementation Plan (by ${plan.generatedBy})`,
|
|
2891
|
+
plan.summary,
|
|
2892
|
+
"Steps:",
|
|
2893
|
+
...plan.steps.map((step) => {
|
|
2894
|
+
const targets = step.targets && step.targets.length > 0 ? ` [targets: ${step.targets.join(", ")}]` : "";
|
|
2895
|
+
return `${step.order}. ${step.title}${targets}`;
|
|
2896
|
+
})
|
|
2897
|
+
];
|
|
2898
|
+
if (plan.risks && plan.risks.length > 0) {
|
|
2899
|
+
lines.push("Risks:", ...plan.risks.map((risk) => `- ${risk}`));
|
|
2900
|
+
}
|
|
2901
|
+
return lines.join("\n");
|
|
2902
|
+
}
|
|
2631
2903
|
function createDefaultChatFn(providers) {
|
|
2632
2904
|
return async (modelRef, messages) => {
|
|
2633
2905
|
const separator = modelRef.indexOf(":");
|
|
@@ -2708,10 +2980,88 @@ var FilePipelineRunStore = class {
|
|
|
2708
2980
|
}
|
|
2709
2981
|
};
|
|
2710
2982
|
|
|
2983
|
+
// src/planner/store.ts
|
|
2984
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
|
|
2985
|
+
import { dirname as dirname5 } from "path";
|
|
2986
|
+
var FilePlanStore = class {
|
|
2987
|
+
filePath;
|
|
2988
|
+
queue = Promise.resolve();
|
|
2989
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
2990
|
+
this.filePath = `${directory}/plans.json`;
|
|
2991
|
+
}
|
|
2992
|
+
async save(plan) {
|
|
2993
|
+
return this.synchronized(async () => {
|
|
2994
|
+
const plans = await this.load();
|
|
2995
|
+
const index = plans.findIndex((p) => p.id === plan.id);
|
|
2996
|
+
if (index >= 0) plans[index] = plan;
|
|
2997
|
+
else plans.push(plan);
|
|
2998
|
+
await this.write(plans);
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
async get(id) {
|
|
3002
|
+
return this.synchronized(async () => {
|
|
3003
|
+
const plans = await this.load();
|
|
3004
|
+
return plans.find((p) => p.id === id);
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
async list(filter = {}) {
|
|
3008
|
+
return this.synchronized(async () => {
|
|
3009
|
+
const plans = await this.load();
|
|
3010
|
+
return plans.filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
synchronized(operation) {
|
|
3014
|
+
const result = this.queue.then(operation, operation);
|
|
3015
|
+
this.queue = result.then(
|
|
3016
|
+
() => void 0,
|
|
3017
|
+
() => void 0
|
|
3018
|
+
);
|
|
3019
|
+
return result;
|
|
3020
|
+
}
|
|
3021
|
+
async load() {
|
|
3022
|
+
try {
|
|
3023
|
+
const raw = await readFile5(this.filePath, "utf8");
|
|
3024
|
+
const parsed = JSON.parse(raw);
|
|
3025
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3026
|
+
} catch {
|
|
3027
|
+
return [];
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
async write(plans) {
|
|
3031
|
+
const tmp = `${this.filePath}.tmp`;
|
|
3032
|
+
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
3033
|
+
await writeFile5(tmp, `${JSON.stringify(plans, null, 2)}
|
|
3034
|
+
`, "utf8");
|
|
3035
|
+
await rename5(tmp, this.filePath);
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
|
|
3039
|
+
// src/planner/factory.ts
|
|
3040
|
+
function buildPlanner(plannerConfig, chat, log = logger) {
|
|
3041
|
+
if (!plannerConfig) return void 0;
|
|
3042
|
+
if (plannerConfig.kind === "heuristic") {
|
|
3043
|
+
return new HeuristicPlanner();
|
|
3044
|
+
}
|
|
3045
|
+
if (!plannerConfig.model) {
|
|
3046
|
+
throw new ConfigError(
|
|
3047
|
+
"CONFIG_INVALID",
|
|
3048
|
+
'Planner kind "model" needs a model reference ("provider:model-id") in omninode.yaml.'
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
return new ModelPlanner({
|
|
3052
|
+
model: plannerConfig.model,
|
|
3053
|
+
chat,
|
|
3054
|
+
...plannerConfig.instruction !== void 0 ? { instruction: plannerConfig.instruction } : {},
|
|
3055
|
+
fallback: new HeuristicPlanner(),
|
|
3056
|
+
log
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2711
3060
|
// src/pipelines/engine-factory.ts
|
|
2712
3061
|
function buildPipelineEngine(config, log = logger) {
|
|
2713
3062
|
const agents = buildAgentRegistry(config, log);
|
|
2714
3063
|
const roles = buildRoleRegistry(config);
|
|
3064
|
+
const chat = createDefaultChatFn(config.project.providers);
|
|
2715
3065
|
const tasks = new TaskEngine({
|
|
2716
3066
|
agents,
|
|
2717
3067
|
roles,
|
|
@@ -2725,7 +3075,9 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
2725
3075
|
roles,
|
|
2726
3076
|
providers: config.project.providers,
|
|
2727
3077
|
store: new FilePipelineRunStore(),
|
|
2728
|
-
chat
|
|
3078
|
+
chat,
|
|
3079
|
+
...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
|
|
3080
|
+
plans: new FilePlanStore(),
|
|
2729
3081
|
reports: new ReportService(new FileReportStore(), log),
|
|
2730
3082
|
log
|
|
2731
3083
|
});
|
|
@@ -2768,6 +3120,9 @@ function registerPipelineCommands(program) {
|
|
|
2768
3120
|
if (finished.combinedReportId) {
|
|
2769
3121
|
console.log(` combined report: ${finished.combinedReportId}`);
|
|
2770
3122
|
}
|
|
3123
|
+
if (finished.planId) {
|
|
3124
|
+
console.log(` plan: ${finished.planId}`);
|
|
3125
|
+
}
|
|
2771
3126
|
console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
|
|
2772
3127
|
if (finished.status !== "completed") process.exitCode = 1;
|
|
2773
3128
|
});
|
|
@@ -2871,6 +3226,52 @@ function registerMemoryCommands(program) {
|
|
|
2871
3226
|
});
|
|
2872
3227
|
}
|
|
2873
3228
|
|
|
3229
|
+
// src/cli/commands/plan.ts
|
|
3230
|
+
function registerPlanCommands(program) {
|
|
3231
|
+
const plan = program.command("plan").description("Inspect implementation plans produced by the planner layer (\xA718).");
|
|
3232
|
+
plan.command("list").description("List stored plans.").option("--run <pipelineRunId>", "Only plans for this pipeline run.").action(async (options) => {
|
|
3233
|
+
const store = new FilePlanStore();
|
|
3234
|
+
const plans = await store.list({
|
|
3235
|
+
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
3236
|
+
});
|
|
3237
|
+
if (plans.length === 0) {
|
|
3238
|
+
console.log("No plans stored yet. Run a pipeline with a plan step to generate one.");
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
for (const p of plans) {
|
|
3242
|
+
console.log(`${p.id} ${p.steps.length} step(s) by ${p.generatedBy} ${truncate(p.summary, 50)}`);
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
plan.command("show <id>").description("Show a stored implementation plan in full.").action(async (id) => {
|
|
3246
|
+
const store = new FilePlanStore();
|
|
3247
|
+
const found = await store.get(id);
|
|
3248
|
+
if (!found) {
|
|
3249
|
+
throw new OmniNodeError("PLAN_INVALID", `Plan "${id}" was not found in the plan store.`, {
|
|
3250
|
+
details: { id }
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
console.log(`Plan ${found.id}`);
|
|
3254
|
+
console.log(` objective: ${found.objective}`);
|
|
3255
|
+
console.log(` by: ${found.generatedBy}`);
|
|
3256
|
+
console.log(` summary: ${found.summary}`);
|
|
3257
|
+
console.log(" steps:");
|
|
3258
|
+
for (const step of [...found.steps].sort((a, b) => a.order - b.order)) {
|
|
3259
|
+
const targets = step.targets && step.targets.length > 0 ? ` [${step.targets.join(", ")}]` : "";
|
|
3260
|
+
console.log(` ${step.order}. ${step.title}${targets}`);
|
|
3261
|
+
if (step.description) console.log(` ${truncate(step.description, 100)}`);
|
|
3262
|
+
for (const criterion of step.acceptanceCriteria ?? []) {
|
|
3263
|
+
console.log(` accept: ${criterion}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
if (found.risks && found.risks.length > 0) {
|
|
3267
|
+
console.log(" risks:");
|
|
3268
|
+
for (const risk of found.risks) {
|
|
3269
|
+
console.log(` - ${risk}`);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
|
|
2874
3275
|
// src/cli/index.ts
|
|
2875
3276
|
function createProgram() {
|
|
2876
3277
|
const program = new Command();
|
|
@@ -2887,6 +3288,7 @@ function createProgram() {
|
|
|
2887
3288
|
registerModelsCommand(program);
|
|
2888
3289
|
registerPipelineCommands(program);
|
|
2889
3290
|
registerMemoryCommands(program);
|
|
3291
|
+
registerPlanCommands(program);
|
|
2890
3292
|
return program;
|
|
2891
3293
|
}
|
|
2892
3294
|
async function run(argv) {
|