omegon 0.6.5 → 0.6.6

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.
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * cleave/dispatcher — Child process dispatch and monitoring.
3
3
  *
4
- * Spawns `pi` subprocesses for each child task, using the same
4
+ * Spawns Omegon-owned subprocesses for each child task, using the same
5
5
  * subagent pattern as pi's example extension. Each child runs in
6
6
  * its own git worktree with an isolated context.
7
7
  *
8
8
  * Supports two backends:
9
- * - "cloud": spawns a full `pi` process (uses cloud API)
10
- * - "local": spawns `pi` with --model pointing to a local Ollama model
9
+ * - "cloud": spawns a full Omegon child process (uses cloud API)
10
+ * - "local": spawns Omegon with --model pointing to a local Ollama model
11
11
  *
12
12
  * The dispatcher handles:
13
13
  * - Dependency-ordered wave execution
@@ -26,6 +26,7 @@ import { computeDispatchWaves } from "./planner.ts";
26
26
  import { executeWithReview, type ReviewConfig, type ReviewExecutor, DEFAULT_REVIEW_CONFIG } from "./review.ts";
27
27
  import { saveState } from "./workspace.ts";
28
28
  import { resolveTier, getDefaultPolicy, getViableModels, type ProviderRoutingPolicy, type RegistryModel } from "../lib/model-routing.ts";
29
+ import { resolveOmegonSubprocess } from "../lib/omegon-subprocess.ts";
29
30
 
30
31
  // ─── Large-run threshold ────────────────────────────────────────────────────
31
32
 
@@ -370,7 +371,8 @@ async function spawnChild(
370
371
  localModel?: string,
371
372
  onLine?: (line: string) => void,
372
373
  ): Promise<ChildResult> {
373
- const args = ["-p", "--no-session"];
374
+ const omegon = resolveOmegonSubprocess();
375
+ const args = [...omegon.argvPrefix, "-p", "--no-session"];
374
376
  if (localModel) {
375
377
  args.push("--model", localModel);
376
378
  }
@@ -380,7 +382,7 @@ async function spawnChild(
380
382
  let stderr = "";
381
383
  let killed = false;
382
384
 
383
- const proc = spawn("pi", args, {
385
+ const proc = spawn(omegon.command, args, {
384
386
  cwd,
385
387
  stdio: ["pipe", "pipe", "pipe"],
386
388
  env: {
@@ -29,6 +29,7 @@ import { debug } from "../lib/debug.ts";
29
29
  import { emitOpenSpecState } from "../openspec/dashboard-state.ts";
30
30
  import { getSharedBridge, buildSlashCommandResult } from "../lib/slash-command-bridge.ts";
31
31
  import { buildAssessBridgeResult } from "./bridge.ts";
32
+ import { resolveOmegonSubprocess } from "../lib/omegon-subprocess.ts";
32
33
  import {
33
34
  assessDirective,
34
35
  PATTERNS,
@@ -755,11 +756,12 @@ async function runSpecAssessmentSubprocess(
755
756
  ...(input.diffContent ? ["### Recent Changes", "", "```diff", input.diffContent, "```", ""] : []),
756
757
  ].join("\n");
757
758
 
758
- const args = ["--mode", "json", "--plan", "-p", "--no-session"];
759
+ const omegon = resolveOmegonSubprocess();
760
+ const args = [...omegon.argvPrefix, "--mode", "json", "--plan", "-p", "--no-session"];
759
761
  if (input.modelId) args.push("--model", input.modelId);
760
762
 
761
763
  return await new Promise<SpecAssessmentRunnerOutput>((resolve, reject) => {
762
- const proc = spawn("pi", args, {
764
+ const proc = spawn(omegon.command, args, {
763
765
  cwd: input.repoPath,
764
766
  shell: false,
765
767
  stdio: ["pipe", "pipe", "pipe"],
@@ -1454,12 +1456,13 @@ async function runDesignAssessmentSubprocess(
1454
1456
  "}",
1455
1457
  ].join("\n");
1456
1458
 
1457
- const args = ["--mode", "json", "--plan", "-p", "--no-session"];
1459
+ const omegon = resolveOmegonSubprocess();
1460
+ const args = [...omegon.argvPrefix, "--mode", "json", "--plan", "-p", "--no-session"];
1458
1461
  if (modelId) args.push("--model", modelId);
1459
1462
 
1460
1463
  return await new Promise<{ findings: DesignAssessmentFinding[]; nodeTitle: string; structuralPass: boolean }>(
1461
1464
  (resolve, reject) => {
1462
- const proc = spawn("pi", args, {
1465
+ const proc = spawn(omegon.command, args, {
1463
1466
  cwd: repoPath,
1464
1467
  shell: false,
1465
1468
  stdio: ["pipe", "pipe", "pipe"],
@@ -0,0 +1,29 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+
4
+ export interface OmegonSubprocessSpec {
5
+ command: string;
6
+ argvPrefix: string[];
7
+ omegonEntry: string;
8
+ }
9
+
10
+ let cached: OmegonSubprocessSpec | null = null;
11
+
12
+ /**
13
+ * Resolve the canonical Omegon-owned subprocess entrypoint without relying on PATH.
14
+ *
15
+ * Internal helpers should spawn `process.execPath` with `bin/omegon.mjs` explicitly,
16
+ * rather than assuming a `pi` or `omegon` binary on PATH points back to this install.
17
+ */
18
+ export function resolveOmegonSubprocess(): OmegonSubprocessSpec {
19
+ if (cached) return cached;
20
+
21
+ const here = dirname(fileURLToPath(import.meta.url));
22
+ const omegonEntry = join(here, "..", "..", "bin", "omegon.mjs");
23
+ cached = {
24
+ command: process.execPath,
25
+ argvPrefix: [omegonEntry],
26
+ omegonEntry,
27
+ };
28
+ return cached;
29
+ }
@@ -15,6 +15,7 @@
15
15
  import { spawn, type ChildProcess } from "node:child_process";
16
16
  import type { MemoryConfig } from "./types.ts";
17
17
  import type { Fact, Edge } from "./factstore.ts";
18
+ import { resolveOmegonSubprocess } from "../lib/omegon-subprocess.ts";
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // Shared subprocess runner
@@ -95,7 +96,9 @@ function spawnExtraction(opts: {
95
96
  return;
96
97
  }
97
98
 
99
+ const omegon = resolveOmegonSubprocess();
98
100
  const args = [
101
+ ...omegon.argvPrefix,
99
102
  "--model", opts.model,
100
103
  "--no-session", "--no-tools", "--no-extensions",
101
104
  "--no-skills", "--no-themes", "--thinking", "off",
@@ -103,7 +106,7 @@ function spawnExtraction(opts: {
103
106
  "-p", opts.userMessage,
104
107
  ];
105
108
 
106
- const proc = spawn("pi", args, {
109
+ const proc = spawn(omegon.command, args, {
107
110
  cwd: opts.cwd,
108
111
  stdio: ["ignore", "pipe", "pipe"],
109
112
  // Detach into new session so child has no controlling terminal.
@@ -91,6 +91,30 @@ function loadDatabase(): any {
91
91
 
92
92
  const Database = loadDatabase();
93
93
 
94
+ function buildSafeFtsQuery(query: string, joiner: "AND" | "OR" = "AND"): string {
95
+ const tokens = query
96
+ .split(/\s+/)
97
+ .map(token => token.trim())
98
+ .filter(token => token.length > 0)
99
+ .map(token => token.replace(/"/g, '""'))
100
+ .filter(token => /[\p{L}\p{N}]/u.test(token))
101
+ .map(token => `"${token}"`);
102
+
103
+ return tokens.join(` ${joiner} `);
104
+ }
105
+
106
+ function isIgnorableFtsQueryError(error: unknown): boolean {
107
+ if (!(error instanceof Error)) return false;
108
+ const message = error.message.toLowerCase();
109
+ return (
110
+ message.includes("fts5")
111
+ || message.includes("malformed match expression")
112
+ || message.includes("syntax error")
113
+ || message.includes("unterminated string")
114
+ || message.includes("no such column")
115
+ );
116
+ }
117
+
94
118
  /** Generate a short unique ID */
95
119
  function nanoid(size = 12): string {
96
120
  const bytes = crypto.randomBytes(size);
@@ -234,6 +258,7 @@ export class FactStore {
234
258
  fs.mkdirSync(memoryDir, { recursive: true });
235
259
  this.db = new Database(this.dbPath);
236
260
  this.db.pragma("journal_mode = WAL");
261
+ this.db.pragma("busy_timeout = 5000");
237
262
  this.db.pragma("foreign_keys = ON");
238
263
  this.initSchema();
239
264
  this.runMigrations();
@@ -1027,47 +1052,56 @@ export class FactStore {
1027
1052
 
1028
1053
  /** Full-text search across all facts (all minds, all statuses) */
1029
1054
  searchFacts(query: string, mind?: string): Fact[] {
1030
- // FTS5 match syntax
1031
- const ftsQuery = query.split(/\s+/).filter(t => t.length > 0).join(" AND ");
1055
+ const ftsQuery = buildSafeFtsQuery(query, "AND");
1032
1056
  if (!ftsQuery) return [];
1033
1057
 
1034
- if (mind) {
1058
+ try {
1059
+ if (mind) {
1060
+ return this.db.prepare(`
1061
+ SELECT f.* FROM facts f
1062
+ JOIN facts_fts fts ON f.rowid = fts.rowid
1063
+ WHERE facts_fts MATCH ? AND f.mind = ?
1064
+ ORDER BY rank
1065
+ `).all(ftsQuery, mind) as Fact[];
1066
+ }
1067
+
1035
1068
  return this.db.prepare(`
1036
1069
  SELECT f.* FROM facts f
1037
1070
  JOIN facts_fts fts ON f.rowid = fts.rowid
1038
- WHERE facts_fts MATCH ? AND f.mind = ?
1071
+ WHERE facts_fts MATCH ?
1039
1072
  ORDER BY rank
1040
- `).all(ftsQuery, mind) as Fact[];
1073
+ `).all(ftsQuery) as Fact[];
1074
+ } catch (error) {
1075
+ if (isIgnorableFtsQueryError(error)) return [];
1076
+ throw error;
1041
1077
  }
1042
-
1043
- return this.db.prepare(`
1044
- SELECT f.* FROM facts f
1045
- JOIN facts_fts fts ON f.rowid = fts.rowid
1046
- WHERE facts_fts MATCH ?
1047
- ORDER BY rank
1048
- `).all(ftsQuery) as Fact[];
1049
1078
  }
1050
1079
 
1051
1080
  /** Search archived/superseded facts (replaces searchArchive) */
1052
1081
  searchArchive(query: string, mind?: string): Fact[] {
1053
- const ftsQuery = query.split(/\s+/).filter(t => t.length > 0).join(" AND ");
1082
+ const ftsQuery = buildSafeFtsQuery(query, "AND");
1054
1083
  if (!ftsQuery) return [];
1055
1084
 
1056
- if (mind) {
1085
+ try {
1086
+ if (mind) {
1087
+ return this.db.prepare(`
1088
+ SELECT f.* FROM facts f
1089
+ JOIN facts_fts fts ON f.rowid = fts.rowid
1090
+ WHERE facts_fts MATCH ? AND f.mind = ? AND f.status IN ('archived', 'superseded')
1091
+ ORDER BY f.created_at DESC
1092
+ `).all(ftsQuery, mind) as Fact[];
1093
+ }
1094
+
1057
1095
  return this.db.prepare(`
1058
1096
  SELECT f.* FROM facts f
1059
1097
  JOIN facts_fts fts ON f.rowid = fts.rowid
1060
- WHERE facts_fts MATCH ? AND f.mind = ? AND f.status IN ('archived', 'superseded')
1098
+ WHERE facts_fts MATCH ? AND f.status IN ('archived', 'superseded')
1061
1099
  ORDER BY f.created_at DESC
1062
- `).all(ftsQuery, mind) as Fact[];
1100
+ `).all(ftsQuery) as Fact[];
1101
+ } catch (error) {
1102
+ if (isIgnorableFtsQueryError(error)) return [];
1103
+ throw error;
1063
1104
  }
1064
-
1065
- return this.db.prepare(`
1066
- SELECT f.* FROM facts f
1067
- JOIN facts_fts fts ON f.rowid = fts.rowid
1068
- WHERE facts_fts MATCH ? AND f.status IN ('archived', 'superseded')
1069
- ORDER BY f.created_at DESC
1070
- `).all(ftsQuery) as Fact[];
1071
1105
  }
1072
1106
 
1073
1107
  /** Get a single fact by ID */
@@ -1850,9 +1884,8 @@ export class FactStore {
1850
1884
  const ftsRanked: Map<string, number> = new Map(); // fact.id → rank (0-indexed)
1851
1885
  if (queryText.length > 2) {
1852
1886
  // Use OR mode for broader recall — AND is too restrictive for injection
1853
- const tokens = queryText.split(/\s+/).filter(t => t.length > 1);
1854
- if (tokens.length > 0) {
1855
- const ftsQuery = tokens.join(" OR ");
1887
+ const ftsQuery = buildSafeFtsQuery(queryText, "OR");
1888
+ if (ftsQuery) {
1856
1889
  try {
1857
1890
  let query = `
1858
1891
  SELECT f.* FROM facts f
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omegon",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Omegon — an opinionated distribution of pi (by Mario Zechner) with extensions for lifecycle management, memory, orchestration, and visualization",
5
5
  "bin": {
6
6
  "omegon": "bin/omegon.mjs",