@hilbras/omninode 2.0.0-alpha.10 → 2.0.0-alpha.12
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 +15 -2
- package/dist/cli/index.js +338 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +197 -19
- package/dist/index.js +585 -349
- 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
|
-
**v2.0.0-alpha.
|
|
21
|
+
**v2.0.0-alpha.12 — v2 Phase 12: Persistence Layer v2**
|
|
22
22
|
(the reliability & interoperability line: architecture → execution →
|
|
23
23
|
protocol → adapters → pipelines → providers → OmniHilbras → memory → reports) (the v2
|
|
24
24
|
roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
|
|
@@ -31,6 +31,19 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
|
|
|
31
31
|
|
|
32
32
|
### What's new in v2 (so far)
|
|
33
33
|
|
|
34
|
+
- **Phase 12 — Persistence Layer v2**: writes are now **fsynced and atomic**
|
|
35
|
+
(temp → fsync → rename → directory fsync), corrupt store files are
|
|
36
|
+
**quarantined instead of lost**, documents carry a **schema version** with
|
|
37
|
+
v1 data migrated in place (`omninode migrate [--check]`), and files from a
|
|
38
|
+
newer OmniNode are refused rather than misread. Details:
|
|
39
|
+
[PERSISTENCE.md](docs/PERSISTENCE.md).
|
|
40
|
+
- **Phase 11 — Planner v2**: the planner now receives the full §15 input —
|
|
41
|
+
task, project context, agent reports, **aggregated findings (consensus and
|
|
42
|
+
conflicts included)**, memory and **constraints** (settable per pipeline
|
|
43
|
+
step). Plans carry an explicit goal, every generated plan is **schema
|
|
44
|
+
validated**, invalid output produces a **structured error** or falls back to
|
|
45
|
+
heuristic planning, and a guard test proves the planner module depends on no
|
|
46
|
+
specific provider.
|
|
34
47
|
- **Phase 10 — Aggregation Improvements**: when agents disagree,
|
|
35
48
|
OmniNode now says so instead of flattening the answer — **conflict
|
|
36
49
|
detection** preserves every position (severity spread, divergent
|
|
@@ -352,7 +365,7 @@ required for the core engine.
|
|
|
352
365
|
- [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
|
|
353
366
|
- [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
|
|
354
367
|
- [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
|
|
355
|
-
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
368
|
+
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Persistence](docs/PERSISTENCE.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
356
369
|
- [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
|
|
357
370
|
|
|
358
371
|
Prefer a container? `docker build -t omninode .` then
|
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 = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.12";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -109,7 +109,9 @@ var pipelineStepConfigSchema = z.object({
|
|
|
109
109
|
model: z.string().optional(),
|
|
110
110
|
depends_on: z.array(z.string()).optional(),
|
|
111
111
|
retries: z.number().int().min(0).max(10).optional(),
|
|
112
|
-
condition: z.enum(["always", "on-success", "on-failure"]).optional()
|
|
112
|
+
condition: z.enum(["always", "on-success", "on-failure"]).optional(),
|
|
113
|
+
/** Constraints passed to the planner for plan steps (§15). */
|
|
114
|
+
constraints: z.array(z.string()).optional()
|
|
113
115
|
}).strict();
|
|
114
116
|
var pipelineConfigSchema = z.object({
|
|
115
117
|
id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
|
|
@@ -239,7 +241,8 @@ function toPipelineDefinition(raw) {
|
|
|
239
241
|
model: step.model,
|
|
240
242
|
dependsOn: step.depends_on,
|
|
241
243
|
retries: step.retries,
|
|
242
|
-
condition: step.condition
|
|
244
|
+
condition: step.condition,
|
|
245
|
+
constraints: step.constraints
|
|
243
246
|
}))
|
|
244
247
|
};
|
|
245
248
|
}
|
|
@@ -2105,51 +2108,119 @@ function registerRoleCommands(program) {
|
|
|
2105
2108
|
}
|
|
2106
2109
|
|
|
2107
2110
|
// src/persistence/json-file-store.ts
|
|
2108
|
-
import { mkdir, readFile, rename,
|
|
2111
|
+
import { open, mkdir, readFile, rename, stat } from "fs/promises";
|
|
2109
2112
|
import { dirname } from "path";
|
|
2113
|
+
var SCHEMA_VERSION = 2;
|
|
2114
|
+
async function writeFileAtomic(target, contents) {
|
|
2115
|
+
const tmp = `${target}.tmp`;
|
|
2116
|
+
await mkdir(dirname(target), { recursive: true });
|
|
2117
|
+
const handle = await open(tmp, "w");
|
|
2118
|
+
try {
|
|
2119
|
+
await handle.writeFile(contents, "utf8");
|
|
2120
|
+
await handle.sync();
|
|
2121
|
+
} finally {
|
|
2122
|
+
await handle.close();
|
|
2123
|
+
}
|
|
2124
|
+
await rename(tmp, target);
|
|
2125
|
+
try {
|
|
2126
|
+
const dir = await open(dirname(target), "r");
|
|
2127
|
+
try {
|
|
2128
|
+
await dir.sync();
|
|
2129
|
+
} finally {
|
|
2130
|
+
await dir.close();
|
|
2131
|
+
}
|
|
2132
|
+
} catch {
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async function inspectDocument(path) {
|
|
2136
|
+
let raw;
|
|
2137
|
+
try {
|
|
2138
|
+
await stat(path);
|
|
2139
|
+
} catch {
|
|
2140
|
+
return { status: "missing" };
|
|
2141
|
+
}
|
|
2142
|
+
try {
|
|
2143
|
+
raw = await readFile(path, "utf8");
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
|
|
2146
|
+
}
|
|
2147
|
+
if (raw.trim().length === 0) return { status: "empty" };
|
|
2148
|
+
let parsed;
|
|
2149
|
+
try {
|
|
2150
|
+
parsed = JSON.parse(raw);
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
return { status: "corrupt", detail: error instanceof Error ? error.message : "invalid JSON" };
|
|
2153
|
+
}
|
|
2154
|
+
if (Array.isArray(parsed)) return { status: "v1-legacy" };
|
|
2155
|
+
if (parsed === null || typeof parsed !== "object") return { status: "corrupt", detail: "not an object" };
|
|
2156
|
+
const doc = parsed;
|
|
2157
|
+
if (!Array.isArray(doc.items)) return { status: "corrupt", detail: "missing items array" };
|
|
2158
|
+
const version = typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1;
|
|
2159
|
+
if (version > SCHEMA_VERSION) return { status: "future", schemaVersion: version };
|
|
2160
|
+
if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
|
|
2161
|
+
return { status: "current", schemaVersion: version, count: doc.items.length };
|
|
2162
|
+
}
|
|
2163
|
+
async function readDocument(path, onCorrupt, quarantine = true) {
|
|
2164
|
+
const state = await inspectDocument(path);
|
|
2165
|
+
if (state.status === "missing" || state.status === "empty") return [];
|
|
2166
|
+
if (state.status === "future") {
|
|
2167
|
+
const error = new Error(
|
|
2168
|
+
`${path} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
|
|
2169
|
+
);
|
|
2170
|
+
onCorrupt?.(path, error);
|
|
2171
|
+
return [];
|
|
2172
|
+
}
|
|
2173
|
+
let raw;
|
|
2174
|
+
try {
|
|
2175
|
+
raw = await readFile(path, "utf8");
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
onCorrupt?.(path, error);
|
|
2178
|
+
return [];
|
|
2179
|
+
}
|
|
2180
|
+
try {
|
|
2181
|
+
const parsed = JSON.parse(raw);
|
|
2182
|
+
if (Array.isArray(parsed)) return parsed;
|
|
2183
|
+
return parsed.items ?? [];
|
|
2184
|
+
} catch (error) {
|
|
2185
|
+
if (quarantine) {
|
|
2186
|
+
const quarantinePath = `${path}.corrupt-${Date.now()}`;
|
|
2187
|
+
try {
|
|
2188
|
+
await rename(path, quarantinePath);
|
|
2189
|
+
logger.child({ component: "persistence" }).warn(
|
|
2190
|
+
`Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
|
|
2191
|
+
);
|
|
2192
|
+
} catch {
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
onCorrupt?.(path, error);
|
|
2196
|
+
return [];
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2110
2199
|
var JsonFileStore = class {
|
|
2111
2200
|
filePath;
|
|
2112
2201
|
schemaVersion;
|
|
2113
2202
|
onCorrupt;
|
|
2203
|
+
quarantine;
|
|
2114
2204
|
log;
|
|
2115
2205
|
queue = Promise.resolve();
|
|
2116
2206
|
constructor(options) {
|
|
2117
2207
|
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
2118
|
-
this.schemaVersion = options.schemaVersion ??
|
|
2208
|
+
this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
|
|
2119
2209
|
this.onCorrupt = options.onCorrupt;
|
|
2210
|
+
this.quarantine = options.quarantine ?? true;
|
|
2120
2211
|
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
2121
2212
|
}
|
|
2122
2213
|
get path() {
|
|
2123
2214
|
return this.filePath;
|
|
2124
2215
|
}
|
|
2125
|
-
/** All items, or [] when missing/corrupt. */
|
|
2216
|
+
/** All items, or [] when missing/migrated-from-v1/corrupt. */
|
|
2126
2217
|
async readAll() {
|
|
2127
|
-
|
|
2128
|
-
try {
|
|
2129
|
-
raw = await readFile(this.filePath, "utf8");
|
|
2130
|
-
} catch {
|
|
2131
|
-
return [];
|
|
2132
|
-
}
|
|
2133
|
-
try {
|
|
2134
|
-
const parsed = JSON.parse(raw);
|
|
2135
|
-
if (Array.isArray(parsed)) return parsed;
|
|
2136
|
-
if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
|
|
2137
|
-
return parsed.items;
|
|
2138
|
-
}
|
|
2139
|
-
throw new Error("persisted file has an unrecognized shape");
|
|
2140
|
-
} catch (error) {
|
|
2141
|
-
this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
|
|
2142
|
-
this.onCorrupt?.(this.filePath, error);
|
|
2143
|
-
return [];
|
|
2144
|
-
}
|
|
2218
|
+
return readDocument(this.filePath, this.onCorrupt, this.quarantine);
|
|
2145
2219
|
}
|
|
2146
2220
|
async writeAll(items) {
|
|
2147
2221
|
const document = { schemaVersion: this.schemaVersion, items };
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
await writeFile(tmp, `${JSON.stringify(document, null, 2)}
|
|
2151
|
-
`, "utf8");
|
|
2152
|
-
await rename(tmp, this.filePath);
|
|
2222
|
+
await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
|
|
2223
|
+
`);
|
|
2153
2224
|
}
|
|
2154
2225
|
/** Serializes an async operation against this file. */
|
|
2155
2226
|
synchronized(operation) {
|
|
@@ -2452,7 +2523,7 @@ function sleep(ms) {
|
|
|
2452
2523
|
}
|
|
2453
2524
|
|
|
2454
2525
|
// src/audit/index.ts
|
|
2455
|
-
import {
|
|
2526
|
+
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2456
2527
|
import { dirname as dirname2 } from "path";
|
|
2457
2528
|
var FileAuditLog = class {
|
|
2458
2529
|
filePath;
|
|
@@ -2464,8 +2535,15 @@ var FileAuditLog = class {
|
|
|
2464
2535
|
async record(event) {
|
|
2465
2536
|
try {
|
|
2466
2537
|
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2467
|
-
await
|
|
2538
|
+
const { open: open2 } = await import("fs/promises");
|
|
2539
|
+
const handle = await open2(this.filePath, "a");
|
|
2540
|
+
try {
|
|
2541
|
+
await handle.appendFile(`${JSON.stringify(event)}
|
|
2468
2542
|
`, "utf8");
|
|
2543
|
+
await handle.sync();
|
|
2544
|
+
} finally {
|
|
2545
|
+
await handle.close();
|
|
2546
|
+
}
|
|
2469
2547
|
} catch (error) {
|
|
2470
2548
|
this.log.warn(
|
|
2471
2549
|
`Audit write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -2474,7 +2552,6 @@ var FileAuditLog = class {
|
|
|
2474
2552
|
}
|
|
2475
2553
|
/** Recent events, oldest first; used by the CLI. */
|
|
2476
2554
|
async recent(limit = 20) {
|
|
2477
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
2478
2555
|
let raw;
|
|
2479
2556
|
try {
|
|
2480
2557
|
raw = await readFile2(this.filePath, "utf8");
|
|
@@ -3532,19 +3609,32 @@ var HeuristicPlanner = class {
|
|
|
3532
3609
|
name = "heuristic";
|
|
3533
3610
|
async plan(request) {
|
|
3534
3611
|
const steps = [];
|
|
3535
|
-
const findings = request.reports.flatMap(
|
|
3536
|
-
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
3537
|
-
);
|
|
3538
3612
|
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
3539
|
-
for (const
|
|
3613
|
+
for (const text of recommendations) {
|
|
3540
3614
|
steps.push({
|
|
3541
3615
|
id: `step-${steps.length + 1}`,
|
|
3542
3616
|
title: truncate5(text, 120),
|
|
3543
3617
|
order: steps.length + 1,
|
|
3544
|
-
...
|
|
3618
|
+
...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
3545
3619
|
});
|
|
3546
3620
|
}
|
|
3547
|
-
const
|
|
3621
|
+
const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
|
|
3622
|
+
finding: {
|
|
3623
|
+
id: `agg-${slug(finding.title)}`,
|
|
3624
|
+
title: finding.title,
|
|
3625
|
+
detail: finding.details[0] ?? finding.title,
|
|
3626
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
3627
|
+
...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
|
|
3628
|
+
},
|
|
3629
|
+
agent: finding.sources.map((s) => s.agent).join("+")
|
|
3630
|
+
})) : request.reports.flatMap(
|
|
3631
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
3632
|
+
);
|
|
3633
|
+
const significant = contributed.filter(
|
|
3634
|
+
({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
|
|
3635
|
+
).sort(
|
|
3636
|
+
(a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
|
|
3637
|
+
);
|
|
3548
3638
|
for (const { finding } of significant) {
|
|
3549
3639
|
steps.push(remediationStep(finding, steps.length + 1));
|
|
3550
3640
|
}
|
|
@@ -3557,12 +3647,20 @@ var HeuristicPlanner = class {
|
|
|
3557
3647
|
});
|
|
3558
3648
|
}
|
|
3559
3649
|
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
3650
|
+
const conflicts = request.aggregated?.conflicts ?? [];
|
|
3651
|
+
if (conflicts.length > 0) {
|
|
3652
|
+
risks.push(
|
|
3653
|
+
`${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
|
|
3654
|
+
);
|
|
3655
|
+
}
|
|
3560
3656
|
return {
|
|
3561
3657
|
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
3562
3658
|
objective: request.objective,
|
|
3659
|
+
goal: request.aggregated?.title ?? truncate5(request.objective, 120),
|
|
3563
3660
|
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
3564
3661
|
steps,
|
|
3565
3662
|
...risks.length > 0 ? { risks } : {},
|
|
3663
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
3566
3664
|
generatedBy: "heuristic",
|
|
3567
3665
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
3568
3666
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
@@ -3577,24 +3675,76 @@ function remediationStep(finding, order) {
|
|
|
3577
3675
|
description: finding.detail,
|
|
3578
3676
|
order,
|
|
3579
3677
|
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
3580
|
-
sourceFindings: [finding.id]
|
|
3678
|
+
sourceFindings: [finding.id],
|
|
3679
|
+
...finding.recommendation ? { recommendation: finding.recommendation } : {}
|
|
3581
3680
|
};
|
|
3582
3681
|
}
|
|
3583
3682
|
function dedupe(texts) {
|
|
3584
3683
|
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
3585
3684
|
}
|
|
3685
|
+
function slug(text) {
|
|
3686
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3687
|
+
}
|
|
3586
3688
|
function truncate5(text, max) {
|
|
3587
3689
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
3588
3690
|
}
|
|
3589
3691
|
|
|
3590
3692
|
// src/planner/model.ts
|
|
3591
3693
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
3694
|
+
|
|
3695
|
+
// src/planner/schema.ts
|
|
3592
3696
|
import { z as z3 } from "zod";
|
|
3697
|
+
var planStepSchema = z3.object({
|
|
3698
|
+
id: z3.string().min(1).optional(),
|
|
3699
|
+
title: z3.string().min(1),
|
|
3700
|
+
description: z3.string().optional(),
|
|
3701
|
+
order: z3.number().int().optional(),
|
|
3702
|
+
targets: z3.array(z3.string()).optional(),
|
|
3703
|
+
acceptance_criteria: z3.array(z3.string()).optional(),
|
|
3704
|
+
depends_on: z3.array(z3.string()).optional(),
|
|
3705
|
+
source_findings: z3.array(z3.string()).optional()
|
|
3706
|
+
});
|
|
3707
|
+
var planJsonSchema = z3.object({
|
|
3708
|
+
goal: z3.string().min(1),
|
|
3709
|
+
summary: z3.string().min(1),
|
|
3710
|
+
steps: z3.array(planStepSchema).min(1),
|
|
3711
|
+
risks: z3.array(z3.string()).optional(),
|
|
3712
|
+
notes: z3.array(z3.string()).optional()
|
|
3713
|
+
});
|
|
3714
|
+
function validatePlan(plan) {
|
|
3715
|
+
const issues = [];
|
|
3716
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
3717
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
3718
|
+
if (stepIds.has(step.id)) {
|
|
3719
|
+
issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
|
|
3720
|
+
}
|
|
3721
|
+
stepIds.add(step.id);
|
|
3722
|
+
if (!step.title || step.title.trim().length === 0) {
|
|
3723
|
+
issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
|
|
3724
|
+
}
|
|
3725
|
+
for (const dep of step.dependsOn ?? []) {
|
|
3726
|
+
if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
|
|
3727
|
+
issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
if (plan.steps.length === 0) {
|
|
3732
|
+
issues.push({ path: "steps", message: "a plan needs at least one step" });
|
|
3733
|
+
}
|
|
3734
|
+
if (!plan.summary || plan.summary.trim().length === 0) {
|
|
3735
|
+
issues.push({ path: "summary", message: "summary must not be empty" });
|
|
3736
|
+
}
|
|
3737
|
+
return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
|
|
3738
|
+
}
|
|
3593
3739
|
|
|
3594
3740
|
// src/planner/context.ts
|
|
3595
3741
|
function buildPlannerContext(request) {
|
|
3596
|
-
const parts = [`#
|
|
3742
|
+
const parts = [`# Task
|
|
3597
3743
|
${request.objective}`];
|
|
3744
|
+
if (request.projectContext) {
|
|
3745
|
+
parts.push(`# Project context
|
|
3746
|
+
${request.projectContext}`);
|
|
3747
|
+
}
|
|
3598
3748
|
if (request.role) {
|
|
3599
3749
|
const roleLines = [`# Role
|
|
3600
3750
|
${request.role.name} (${request.role.id})`];
|
|
@@ -3614,7 +3764,7 @@ ${request.role.name} (${request.role.id})`];
|
|
|
3614
3764
|
)
|
|
3615
3765
|
);
|
|
3616
3766
|
parts.push(
|
|
3617
|
-
`#
|
|
3767
|
+
`# Agent reports (${request.reports.length} by ${agents})
|
|
3618
3768
|
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
3619
3769
|
);
|
|
3620
3770
|
const recommendationLines = request.reports.flatMap(
|
|
@@ -3624,9 +3774,20 @@ ${request.role.name} (${request.role.id})`];
|
|
|
3624
3774
|
parts.push(`# Recommendations
|
|
3625
3775
|
${recommendationLines.join("\n")}`);
|
|
3626
3776
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3777
|
+
}
|
|
3778
|
+
if (request.aggregated) {
|
|
3779
|
+
const aggregatedLines = [
|
|
3780
|
+
`- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
|
|
3781
|
+
...request.aggregated.findings.map(
|
|
3782
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
|
|
3783
|
+
),
|
|
3784
|
+
// Conflicts stay visible to the planner — it must plan around disagreement.
|
|
3785
|
+
...request.aggregated.conflicts.map(
|
|
3786
|
+
(conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
|
|
3787
|
+
)
|
|
3788
|
+
];
|
|
3789
|
+
parts.push(`# Aggregated findings
|
|
3790
|
+
${aggregatedLines.join("\n")}`);
|
|
3630
3791
|
}
|
|
3631
3792
|
if (request.memory && request.memory.length > 0) {
|
|
3632
3793
|
parts.push(
|
|
@@ -3634,7 +3795,11 @@ ${summaryLines.join("\n")}`);
|
|
|
3634
3795
|
` + request.memory.map((entry2) => `- [${entry2.scope}] ${entry2.content}`).join("\n")
|
|
3635
3796
|
);
|
|
3636
3797
|
}
|
|
3637
|
-
if (request.
|
|
3798
|
+
if (request.constraints && request.constraints.length > 0) {
|
|
3799
|
+
parts.push(`# Constraints
|
|
3800
|
+
${request.constraints.map((c) => `- ${c}`).join("\n")}`);
|
|
3801
|
+
}
|
|
3802
|
+
if (request.context) {
|
|
3638
3803
|
parts.push(`# Research context
|
|
3639
3804
|
${request.context}`);
|
|
3640
3805
|
}
|
|
@@ -3642,22 +3807,6 @@ ${request.context}`);
|
|
|
3642
3807
|
}
|
|
3643
3808
|
|
|
3644
3809
|
// src/planner/model.ts
|
|
3645
|
-
var planStepJsonSchema = z3.object({
|
|
3646
|
-
id: z3.string().optional(),
|
|
3647
|
-
title: z3.string().min(1),
|
|
3648
|
-
description: z3.string().optional(),
|
|
3649
|
-
order: z3.number().int().optional(),
|
|
3650
|
-
targets: z3.array(z3.string()).optional(),
|
|
3651
|
-
acceptance_criteria: z3.array(z3.string()).optional(),
|
|
3652
|
-
depends_on: z3.array(z3.string()).optional(),
|
|
3653
|
-
source_findings: z3.array(z3.string()).optional()
|
|
3654
|
-
}).strict();
|
|
3655
|
-
var planJsonSchema = z3.object({
|
|
3656
|
-
summary: z3.string().min(1),
|
|
3657
|
-
steps: z3.array(planStepJsonSchema).min(1),
|
|
3658
|
-
risks: z3.array(z3.string()).optional(),
|
|
3659
|
-
notes: z3.array(z3.string()).optional()
|
|
3660
|
-
}).strict();
|
|
3661
3810
|
var ModelPlanner = class {
|
|
3662
3811
|
constructor(options) {
|
|
3663
3812
|
this.options = options;
|
|
@@ -3671,7 +3820,7 @@ var ModelPlanner = class {
|
|
|
3671
3820
|
const messages = [
|
|
3672
3821
|
{
|
|
3673
3822
|
role: "system",
|
|
3674
|
-
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 ?? "")
|
|
3823
|
+
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: {"goal": string, "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 ?? "")
|
|
3675
3824
|
},
|
|
3676
3825
|
{ role: "user", content: context }
|
|
3677
3826
|
];
|
|
@@ -3682,60 +3831,86 @@ var ModelPlanner = class {
|
|
|
3682
3831
|
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
3683
3832
|
}
|
|
3684
3833
|
const parsed = parsePlanJson(responseText);
|
|
3685
|
-
if (!parsed) {
|
|
3686
|
-
return this.degrade(request,
|
|
3834
|
+
if (!parsed.ok) {
|
|
3835
|
+
return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
|
|
3687
3836
|
}
|
|
3688
3837
|
const plan = {
|
|
3689
3838
|
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
3690
3839
|
objective: request.objective,
|
|
3840
|
+
goal: parsed.goal,
|
|
3691
3841
|
summary: parsed.summary,
|
|
3692
3842
|
steps: parsed.steps,
|
|
3693
3843
|
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
3694
3844
|
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
3845
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
3695
3846
|
generatedBy: `model:${this.options.model}`,
|
|
3696
3847
|
model: this.options.model,
|
|
3697
3848
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
3698
3849
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
3699
3850
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3700
3851
|
};
|
|
3701
|
-
|
|
3852
|
+
const validation = validatePlan(plan);
|
|
3853
|
+
if (!validation.valid) {
|
|
3854
|
+
return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
|
|
3855
|
+
}
|
|
3856
|
+
return validation.plan;
|
|
3702
3857
|
}
|
|
3703
|
-
async degrade(request, reason) {
|
|
3858
|
+
async degrade(request, reason, issues = []) {
|
|
3704
3859
|
if (this.options.fallback) {
|
|
3705
3860
|
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
3706
3861
|
return this.options.fallback.plan(request);
|
|
3707
3862
|
}
|
|
3708
|
-
throw new PlannerError("
|
|
3863
|
+
throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
|
|
3864
|
+
details: { reason, issues }
|
|
3865
|
+
});
|
|
3709
3866
|
}
|
|
3710
3867
|
};
|
|
3711
3868
|
function parsePlanJson(text) {
|
|
3712
3869
|
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
3713
3870
|
const start = unfenced.indexOf("{");
|
|
3714
3871
|
const end = unfenced.lastIndexOf("}");
|
|
3715
|
-
if (start < 0 || end <= start)
|
|
3872
|
+
if (start < 0 || end <= start) {
|
|
3873
|
+
return { ok: false, reason: "planner output contained no JSON object", issues: [] };
|
|
3874
|
+
}
|
|
3875
|
+
let json;
|
|
3716
3876
|
try {
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
if (!parsed.success) return void 0;
|
|
3720
|
-
const steps = parsed.data.steps.map((step, index) => ({
|
|
3721
|
-
id: step.id ?? `step-${index + 1}`,
|
|
3722
|
-
title: step.title,
|
|
3723
|
-
description: step.description,
|
|
3724
|
-
order: step.order ?? index + 1,
|
|
3725
|
-
targets: step.targets,
|
|
3726
|
-
acceptanceCriteria: step.acceptance_criteria,
|
|
3727
|
-
dependsOn: step.depends_on,
|
|
3728
|
-
sourceFindings: step.source_findings
|
|
3729
|
-
}));
|
|
3877
|
+
json = JSON.parse(unfenced.slice(start, end + 1));
|
|
3878
|
+
} catch (error) {
|
|
3730
3879
|
return {
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3880
|
+
ok: false,
|
|
3881
|
+
reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
3882
|
+
issues: []
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
3886
|
+
if (!parsed.success) {
|
|
3887
|
+
return {
|
|
3888
|
+
ok: false,
|
|
3889
|
+
reason: "planner output did not match the plan schema",
|
|
3890
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
3891
|
+
path: issue.path.join(".") || "(root)",
|
|
3892
|
+
message: issue.message
|
|
3893
|
+
}))
|
|
3735
3894
|
};
|
|
3736
|
-
} catch {
|
|
3737
|
-
return void 0;
|
|
3738
3895
|
}
|
|
3896
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
3897
|
+
id: step.id ?? `step-${index + 1}`,
|
|
3898
|
+
title: step.title,
|
|
3899
|
+
description: step.description,
|
|
3900
|
+
order: step.order ?? index + 1,
|
|
3901
|
+
targets: step.targets,
|
|
3902
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
3903
|
+
dependsOn: step.depends_on,
|
|
3904
|
+
sourceFindings: step.source_findings
|
|
3905
|
+
})).sort((a, b) => a.order - b.order);
|
|
3906
|
+
return {
|
|
3907
|
+
ok: true,
|
|
3908
|
+
goal: parsed.data.goal,
|
|
3909
|
+
summary: parsed.data.summary,
|
|
3910
|
+
steps,
|
|
3911
|
+
risks: parsed.data.risks,
|
|
3912
|
+
notes: parsed.data.notes
|
|
3913
|
+
};
|
|
3739
3914
|
}
|
|
3740
3915
|
function describe(error) {
|
|
3741
3916
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -4173,11 +4348,14 @@ ${contribution}`);
|
|
|
4173
4348
|
};
|
|
4174
4349
|
}
|
|
4175
4350
|
async runPlan(step, ctx) {
|
|
4351
|
+
const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports) : void 0;
|
|
4176
4352
|
const request = {
|
|
4177
4353
|
objective: ctx.objective,
|
|
4178
4354
|
reports: ctx.reports,
|
|
4355
|
+
...aggregated !== void 0 ? { aggregated } : {},
|
|
4179
4356
|
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
4180
4357
|
...ctx.role ? { role: ctx.role } : {},
|
|
4358
|
+
...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
|
|
4181
4359
|
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
4182
4360
|
};
|
|
4183
4361
|
let planner;
|
|
@@ -4705,6 +4883,78 @@ function registerAuditCommand(program) {
|
|
|
4705
4883
|
});
|
|
4706
4884
|
}
|
|
4707
4885
|
|
|
4886
|
+
// src/persistence/index.ts
|
|
4887
|
+
var STORE_FILES = [
|
|
4888
|
+
"tasks.json",
|
|
4889
|
+
"pipelines.json",
|
|
4890
|
+
"reports.json",
|
|
4891
|
+
"plans.json",
|
|
4892
|
+
"memory.json"
|
|
4893
|
+
];
|
|
4894
|
+
async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
|
|
4895
|
+
const reports = [];
|
|
4896
|
+
for (const file of STORE_FILES) {
|
|
4897
|
+
const path = `${directory}/${file}`;
|
|
4898
|
+
const state = await inspectDocument(path);
|
|
4899
|
+
if (state.status === "missing") {
|
|
4900
|
+
reports.push({ file, state: state.status, migrated: false });
|
|
4901
|
+
continue;
|
|
4902
|
+
}
|
|
4903
|
+
if (state.status === "current") {
|
|
4904
|
+
reports.push({ file, state: state.status, before: state.count, after: state.count, migrated: false });
|
|
4905
|
+
continue;
|
|
4906
|
+
}
|
|
4907
|
+
if (state.status === "future") {
|
|
4908
|
+
reports.push({
|
|
4909
|
+
file,
|
|
4910
|
+
state: state.status,
|
|
4911
|
+
migrated: false,
|
|
4912
|
+
note: `written by a newer OmniNode (schema v${state.schemaVersion}); not touched`
|
|
4913
|
+
});
|
|
4914
|
+
continue;
|
|
4915
|
+
}
|
|
4916
|
+
if (state.status === "corrupt") {
|
|
4917
|
+
reports.push({ file, state: state.status, migrated: false, note: state.detail });
|
|
4918
|
+
continue;
|
|
4919
|
+
}
|
|
4920
|
+
const items = await readDocument(path, void 0, false);
|
|
4921
|
+
if (!options.checkOnly) {
|
|
4922
|
+
await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
|
|
4923
|
+
`);
|
|
4924
|
+
}
|
|
4925
|
+
const beforeCount = items.length;
|
|
4926
|
+
reports.push({
|
|
4927
|
+
file,
|
|
4928
|
+
state: state.status,
|
|
4929
|
+
before: beforeCount,
|
|
4930
|
+
after: items.length,
|
|
4931
|
+
migrated: !options.checkOnly,
|
|
4932
|
+
...options.checkOnly ? { note: "would migrate" } : {}
|
|
4933
|
+
});
|
|
4934
|
+
}
|
|
4935
|
+
return reports;
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4938
|
+
// src/cli/commands/migrate.ts
|
|
4939
|
+
function registerMigrateCommand(program) {
|
|
4940
|
+
program.command("migrate").description(
|
|
4941
|
+
`Inspect and upgrade persisted store files to schema v${SCHEMA_VERSION} (v1 data is migrated in place).`
|
|
4942
|
+
).option("--check", "Only report what would change; do not write.").action(async (options) => {
|
|
4943
|
+
const reports = await migrateProjectStores(`${process.cwd()}/.omninode`, {
|
|
4944
|
+
checkOnly: options.check ?? false
|
|
4945
|
+
});
|
|
4946
|
+
let changed = 0;
|
|
4947
|
+
for (const report of reports) {
|
|
4948
|
+
if (report.migrated || report.state === "v1-legacy") changed += 1;
|
|
4949
|
+
const detail = report.state === "corrupt" ? ` (corrupt: ${report.note})` : report.state === "future" ? ` (${report.note})` : report.state === "v1-legacy" ? ` ${report.migrated ? "migrated" : "would migrate"} ${report.before ?? 0} \u2192 ${report.after ?? 0} item(s)` : report.state === "current" ? ` ${report.after ?? 0} item(s)` : "";
|
|
4950
|
+
console.log(`${report.file.padEnd(16)} ${report.state}${detail}`);
|
|
4951
|
+
}
|
|
4952
|
+
console.log(
|
|
4953
|
+
changed === 0 ? `All stores are at schema v${SCHEMA_VERSION}.` : `${options.check ? "Would migrate" : "Migrated"} ${changed} store file(s) to schema v${SCHEMA_VERSION}.`
|
|
4954
|
+
);
|
|
4955
|
+
});
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4708
4958
|
// src/cli/index.ts
|
|
4709
4959
|
function createProgram() {
|
|
4710
4960
|
const program = new Command();
|
|
@@ -4724,6 +4974,7 @@ function createProgram() {
|
|
|
4724
4974
|
registerPlanCommands(program);
|
|
4725
4975
|
registerStatusCommand(program);
|
|
4726
4976
|
registerAuditCommand(program);
|
|
4977
|
+
registerMigrateCommand(program);
|
|
4727
4978
|
registerRunAlias(program);
|
|
4728
4979
|
return program;
|
|
4729
4980
|
}
|