@hilbras/omninode 0.7.0 → 0.8.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.7.0 — Phase 6: Multi-AI Report System** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
21
+ **v0.8.0 — Phase 7: Memory Integration** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
22
22
  for the full plan and roadmap).
23
23
 
24
24
  What exists today:
@@ -74,6 +74,16 @@ What exists today:
74
74
  - Aggregation (§17): duplicate/similar findings merged via deterministic
75
75
  token-set similarity, sources preserved, highest severity kept
76
76
  - Combined reports generated automatically at the end of every pipeline run
77
+ - **Memory** (`src/memory/`): the §20 loop — before a task runs, relevant
78
+ memory is gathered and injected into the agent's context; after it finishes,
79
+ the outcome is recorded back. High/critical findings are promoted to
80
+ project-level "known problems". Opt in via a `memory:` config section:
81
+ - `provider: local` (default) — JSON file at `.omninode/memory.json` with
82
+ deterministic keyword/tag relevance scoring, zero dependencies
83
+ - `provider: remembera` — the preferred Hilbras memory integration over
84
+ HTTP (base URL + `api_key_env_var`); strictly optional, like every
85
+ Hilbras component
86
+ - Memory failures are best-effort: logged, never fatal to a task
77
87
  - Typed error hierarchy (`src/errors/`)
78
88
  - Leveled logger with pluggable sink (`src/logger/`)
79
89
  - Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
@@ -156,6 +166,15 @@ omninode report combined # the aggregated intelligence of that run
156
166
  omninode report show combined-xxx # grouped findings with per-source attribution
157
167
  ```
158
168
 
169
+ Persistent memory (§19–§20) — opt in with `memory: { provider: local }` or
170
+ `provider: remembera` in `omninode.yaml`:
171
+
172
+ ```bash
173
+ omninode memory status # provider + entry counts
174
+ omninode memory query "authentication jwt" # what the agents will see
175
+ omninode memory write "Decision: use rotating JWT secrets" --scope project --tags architecture
176
+ ```
177
+
159
178
  See [omninode.yaml.example](omninode.yaml.example) for a full configuration.
160
179
 
161
180
  ### Configuration
@@ -190,8 +209,8 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
190
209
  | v0.3.0 | OmniHilbras Integration (optional, like every provider) |
191
210
  | v0.4.0 | Agent System |
192
211
  | v0.5.0 | Roles & Tasks |
193
- | **v0.7.0** | **Multi-AI Reports (current)** |
194
- | v0.8.0 | Remembera (optional memory integration) |
212
+ | v0.7.0 | Multi-AI Reports |
213
+ | **v0.8.0** | **Remembera / Memory (current)** |
195
214
  | v0.9.0 | Planner |
196
215
  | v0.10.0 | End-to-End Runtime |
197
216
  | v1.0.0 | Production Release |
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.7.0";
9
+ var OMNINODE_VERSION = "0.8.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 MemoryError = class extends OmniNodeError {
53
+ constructor(message, options = {}) {
54
+ super("MEMORY_UNAVAILABLE", message, options);
55
+ }
56
+ };
52
57
  function isOmniNodeError(value) {
53
58
  return value instanceof OmniNodeError;
54
59
  }
@@ -112,7 +117,11 @@ var projectConfigSchema = z.object({
112
117
  agents: z.array(agentConfigSchema).default([]),
113
118
  roles: z.array(roleConfigSchema).default([]),
114
119
  pipelines: z.array(pipelineConfigSchema).default([]),
115
- memory: z.object({ provider: z.string().min(1) }).strict().optional()
120
+ memory: z.object({
121
+ provider: z.string().min(1).default("local"),
122
+ base_url: z.string().url().optional(),
123
+ api_key_env_var: z.string().min(1).optional()
124
+ }).strict().optional()
116
125
  }).strict();
117
126
  var appConfigSchema = z.object({
118
127
  project: projectConfigSchema,
@@ -182,7 +191,13 @@ function toAppConfig(yaml) {
182
191
  agents: yaml.project.agents.map(toAgentConfig),
183
192
  roles: yaml.project.roles.map(toRoleDefinition),
184
193
  pipelines: yaml.project.pipelines.map(toPipelineDefinition),
185
- ...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
194
+ ...yaml.project.memory !== void 0 ? {
195
+ memory: {
196
+ provider: yaml.project.memory.provider,
197
+ baseUrl: yaml.project.memory.base_url,
198
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var
199
+ }
200
+ } : {}
186
201
  },
187
202
  ...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
188
203
  };
@@ -1389,13 +1404,24 @@ var TaskEngine = class {
1389
1404
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1390
1405
  const running = await this.transition(task, "running");
1391
1406
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1407
+ let background = running.context?.background;
1408
+ if (this.options.memory) {
1409
+ const memoryContext = await this.options.memory.gatherContext({
1410
+ objective: running.objective,
1411
+ ...running.role ? { role: running.role } : {},
1412
+ ...running.project ? { project: running.project } : {}
1413
+ });
1414
+ if (memoryContext.length > 0) {
1415
+ background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1416
+ }
1417
+ }
1392
1418
  let output;
1393
1419
  try {
1394
1420
  output = await agent.run({
1395
1421
  taskId: running.id,
1396
1422
  objective: running.objective,
1397
1423
  ...role ? { role } : {},
1398
- ...running.context?.background ? { context: running.context.background } : {}
1424
+ ...background !== void 0 ? { context: background } : {}
1399
1425
  });
1400
1426
  } catch (error) {
1401
1427
  const message = error instanceof Error ? error.message : String(error);
@@ -1413,8 +1439,12 @@ var TaskEngine = class {
1413
1439
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1414
1440
  };
1415
1441
  const finalStatus = output.status === "completed" ? "completed" : "failed";
1442
+ const finished = await this.transition(running, finalStatus, { result });
1416
1443
  this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1417
- return this.transition(running, finalStatus, { result });
1444
+ if (this.options.memory) {
1445
+ await this.options.memory.recordTaskOutcome(finished);
1446
+ }
1447
+ return finished;
1418
1448
  }
1419
1449
  async cancel(id) {
1420
1450
  const task = await this.mustGet(id);
@@ -1459,6 +1489,297 @@ function generateTaskId() {
1459
1489
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1460
1490
  }
1461
1491
 
1492
+ // src/memory/local.ts
1493
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1494
+ import { dirname as dirname2 } from "path";
1495
+ var STOPWORDS = /* @__PURE__ */ new Set([
1496
+ "the",
1497
+ "and",
1498
+ "for",
1499
+ "with",
1500
+ "this",
1501
+ "that",
1502
+ "from",
1503
+ "into",
1504
+ "was",
1505
+ "were",
1506
+ "have",
1507
+ "has",
1508
+ "had",
1509
+ "are",
1510
+ "but",
1511
+ "not",
1512
+ "you",
1513
+ "his",
1514
+ "her",
1515
+ "its",
1516
+ "their",
1517
+ "them",
1518
+ "then",
1519
+ "than",
1520
+ "when",
1521
+ "what",
1522
+ "which",
1523
+ "will",
1524
+ "would",
1525
+ "been",
1526
+ "were",
1527
+ "also",
1528
+ "because",
1529
+ "about",
1530
+ "after",
1531
+ "before"
1532
+ ]);
1533
+ function keywords(text) {
1534
+ return new Set(
1535
+ (text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
1536
+ );
1537
+ }
1538
+ function relevanceScore(queryKeywords, entry2) {
1539
+ if (queryKeywords.size === 0) return 0;
1540
+ const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
1541
+ let overlap = 0;
1542
+ for (const keyword of queryKeywords) {
1543
+ if (entryKeywords.has(keyword)) overlap += 1;
1544
+ }
1545
+ let tagMatches = 0;
1546
+ for (const tag of entry2.tags ?? []) {
1547
+ if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
1548
+ }
1549
+ return overlap * 2 + tagMatches * 1.5;
1550
+ }
1551
+ var LocalMemoryProvider = class {
1552
+ name = "local";
1553
+ filePath;
1554
+ queue = Promise.resolve();
1555
+ constructor(directory = `${process.cwd()}/.omninode`) {
1556
+ this.filePath = `${directory}/memory.json`;
1557
+ }
1558
+ async query(query) {
1559
+ return this.synchronized(async () => {
1560
+ const entries = await this.load();
1561
+ const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
1562
+ (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
1563
+ );
1564
+ if (!query.text || query.text.trim().length === 0) {
1565
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1566
+ }
1567
+ const queryKeywords = keywords(query.text);
1568
+ return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
1569
+ });
1570
+ }
1571
+ async write(entry2) {
1572
+ return this.synchronized(async () => {
1573
+ const entries = await this.load();
1574
+ const index = entries.findIndex((e) => e.key === entry2.key);
1575
+ const stamped = {
1576
+ ...entry2,
1577
+ createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1578
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1579
+ };
1580
+ if (index >= 0) entries[index] = stamped;
1581
+ else entries.push(stamped);
1582
+ await this.saveAll(entries);
1583
+ });
1584
+ }
1585
+ async count() {
1586
+ return (await this.load()).length;
1587
+ }
1588
+ synchronized(operation) {
1589
+ const result = this.queue.then(operation, operation);
1590
+ this.queue = result.then(
1591
+ () => void 0,
1592
+ () => void 0
1593
+ );
1594
+ return result;
1595
+ }
1596
+ async load() {
1597
+ try {
1598
+ const raw = await readFile2(this.filePath, "utf8");
1599
+ const parsed = JSON.parse(raw);
1600
+ return Array.isArray(parsed) ? parsed : [];
1601
+ } catch {
1602
+ return [];
1603
+ }
1604
+ }
1605
+ async saveAll(entries) {
1606
+ const tmp = `${this.filePath}.tmp`;
1607
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1608
+ await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1609
+ `, "utf8");
1610
+ await rename2(tmp, this.filePath);
1611
+ }
1612
+ };
1613
+
1614
+ // src/memory/remembera.ts
1615
+ var RememberaMemoryProvider = class {
1616
+ name = "remembera";
1617
+ baseUrl;
1618
+ apiKeyEnvVar;
1619
+ env;
1620
+ log;
1621
+ constructor(config, log = logger) {
1622
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
1623
+ this.apiKeyEnvVar = config.apiKeyEnvVar;
1624
+ this.env = config.env ?? process.env;
1625
+ this.log = log.child({ component: "remembera" });
1626
+ }
1627
+ headers() {
1628
+ const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1629
+ return key ? { authorization: `Bearer ${key}` } : {};
1630
+ }
1631
+ async query(query) {
1632
+ const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1633
+ if (status !== 200) {
1634
+ throw new MemoryError(`Remembera query returned HTTP ${status}.`);
1635
+ }
1636
+ const entries = body.entries;
1637
+ if (!Array.isArray(entries)) {
1638
+ throw new MemoryError("Remembera query response had no entries array.");
1639
+ }
1640
+ return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
1641
+ }
1642
+ async write(entry2) {
1643
+ const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
1644
+ if (status !== 200 && status !== 201) {
1645
+ throw new MemoryError(`Remembera write returned HTTP ${status}.`);
1646
+ }
1647
+ }
1648
+ async request(url, body) {
1649
+ const headers = this.headers();
1650
+ try {
1651
+ return await requestJson(url, {
1652
+ method: "POST",
1653
+ headers,
1654
+ body
1655
+ });
1656
+ } catch (error) {
1657
+ const message = error instanceof Error ? error.message : String(error);
1658
+ throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
1659
+ }
1660
+ }
1661
+ toEntry(raw) {
1662
+ if (raw === null || typeof raw !== "object") {
1663
+ this.log.warn("Skipping malformed Remembera memory entry.");
1664
+ return void 0;
1665
+ }
1666
+ const entry2 = raw;
1667
+ if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
1668
+ this.log.warn("Skipping Remembera memory entry without key/content.");
1669
+ return void 0;
1670
+ }
1671
+ const scope = entry2.scope;
1672
+ return {
1673
+ key: entry2.key,
1674
+ content: entry2.content,
1675
+ scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1676
+ ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
1677
+ ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
1678
+ ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
1679
+ };
1680
+ }
1681
+ };
1682
+
1683
+ // src/memory/factory.ts
1684
+ function createMemoryProvider(config) {
1685
+ if (!config || config.provider === "local") {
1686
+ return new LocalMemoryProvider();
1687
+ }
1688
+ if (config.provider === "remembera") {
1689
+ if (!config.baseUrl) {
1690
+ throw new ConfigError(
1691
+ "CONFIG_INVALID",
1692
+ `Memory provider "remembera" needs a base_url in omninode.yaml.`
1693
+ );
1694
+ }
1695
+ return new RememberaMemoryProvider({
1696
+ baseUrl: config.baseUrl,
1697
+ apiKeyEnvVar: config.apiKeyEnvVar
1698
+ });
1699
+ }
1700
+ throw new ConfigError(
1701
+ "CONFIG_INVALID",
1702
+ `Unknown memory provider "${config.provider}". Available: local, remembera.`
1703
+ );
1704
+ }
1705
+
1706
+ // src/memory/service.ts
1707
+ var MAX_CONTEXT_CHARS = 4e3;
1708
+ var MemoryService = class {
1709
+ constructor(provider, log = logger) {
1710
+ this.provider = provider;
1711
+ this.log = log.child({ component: "memory", provider: provider.name });
1712
+ }
1713
+ provider;
1714
+ log;
1715
+ get providerRef() {
1716
+ return this.provider;
1717
+ }
1718
+ get providerName() {
1719
+ return this.provider.name;
1720
+ }
1721
+ /** Relevant context for a task objective; empty string when nothing matches. */
1722
+ async gatherContext(query) {
1723
+ try {
1724
+ const entries = await this.provider.query({
1725
+ text: query.objective,
1726
+ ...query.role ? { tags: [query.role] } : {},
1727
+ limit: 8
1728
+ });
1729
+ if (entries.length === 0) return "";
1730
+ const byScope = /* @__PURE__ */ new Map();
1731
+ for (const entry2 of entries) {
1732
+ byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
1733
+ }
1734
+ const parts = [];
1735
+ for (const scope of ["project", "task", "role", "knowledge"]) {
1736
+ const scopeEntries = byScope.get(scope) ?? [];
1737
+ if (scopeEntries.length === 0) continue;
1738
+ const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
1739
+ parts.push(`### Relevant memory (${scope})
1740
+ ${lines}`);
1741
+ }
1742
+ const context = parts.join("\n\n");
1743
+ return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1744
+ } catch (error) {
1745
+ this.log.warn(
1746
+ `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1747
+ );
1748
+ return "";
1749
+ }
1750
+ }
1751
+ /** Records a finished task's outcome — best-effort. */
1752
+ async recordTaskOutcome(task) {
1753
+ const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1754
+ try {
1755
+ await this.provider.write({
1756
+ key: `task:${task.id}`,
1757
+ scope: "task",
1758
+ content: `Objective: ${task.objective}
1759
+ Status: ${task.status}
1760
+ Outcome: ${outcome}`,
1761
+ tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
1762
+ createdAt: task.createdAt
1763
+ });
1764
+ const significant = (task.result?.reports ?? []).flatMap(
1765
+ (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1766
+ );
1767
+ for (const { report, finding } of significant) {
1768
+ await this.provider.write({
1769
+ key: `finding:${report.id}:${finding.id}`,
1770
+ scope: "project",
1771
+ content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1772
+ tags: ["known-problem", report.agent]
1773
+ });
1774
+ }
1775
+ } catch (error) {
1776
+ this.log.warn(
1777
+ `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
1778
+ );
1779
+ }
1780
+ }
1781
+ };
1782
+
1462
1783
  // src/roles/index.ts
1463
1784
  var RoleRegistry = class {
1464
1785
  roles = /* @__PURE__ */ new Map();
@@ -1483,10 +1804,13 @@ function buildRoleRegistry(config) {
1483
1804
 
1484
1805
  // src/tasks/engine-factory.ts
1485
1806
  function createTaskEngine(config, log = logger, directory) {
1807
+ const agents = buildAgentRegistry(config, log);
1808
+ const roles = buildRoleRegistry(config);
1486
1809
  return new TaskEngine({
1487
- agents: buildAgentRegistry(config, log),
1488
- roles: buildRoleRegistry(config),
1810
+ agents,
1811
+ roles,
1489
1812
  store: new FileTaskStore(directory),
1813
+ ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
1490
1814
  log
1491
1815
  });
1492
1816
  }
@@ -1788,8 +2112,8 @@ var ReportService = class {
1788
2112
  };
1789
2113
 
1790
2114
  // src/reports/store.ts
1791
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1792
- import { dirname as dirname2 } from "path";
2115
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2116
+ import { dirname as dirname3 } from "path";
1793
2117
  var FileReportStore = class {
1794
2118
  filePath;
1795
2119
  queue = Promise.resolve();
@@ -1853,7 +2177,7 @@ var FileReportStore = class {
1853
2177
  }
1854
2178
  async load() {
1855
2179
  try {
1856
- const raw = await readFile2(this.filePath, "utf8");
2180
+ const raw = await readFile3(this.filePath, "utf8");
1857
2181
  const parsed = JSON.parse(raw);
1858
2182
  return Array.isArray(parsed) ? parsed : [];
1859
2183
  } catch {
@@ -1862,10 +2186,10 @@ var FileReportStore = class {
1862
2186
  }
1863
2187
  async write(entries) {
1864
2188
  const tmp = `${this.filePath}.tmp`;
1865
- await mkdir2(dirname2(this.filePath), { recursive: true });
1866
- await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
2189
+ await mkdir3(dirname3(this.filePath), { recursive: true });
2190
+ await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
1867
2191
  `, "utf8");
1868
- await rename2(tmp, this.filePath);
2192
+ await rename3(tmp, this.filePath);
1869
2193
  }
1870
2194
  };
1871
2195
 
@@ -2329,8 +2653,8 @@ function createDefaultChatFn(providers) {
2329
2653
  }
2330
2654
 
2331
2655
  // src/pipelines/store.ts
2332
- import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2333
- import { dirname as dirname3 } from "path";
2656
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2657
+ import { dirname as dirname4 } from "path";
2334
2658
  var FilePipelineRunStore = class {
2335
2659
  filePath;
2336
2660
  queue = Promise.resolve();
@@ -2368,7 +2692,7 @@ var FilePipelineRunStore = class {
2368
2692
  }
2369
2693
  async load() {
2370
2694
  try {
2371
- const raw = await readFile3(this.filePath, "utf8");
2695
+ const raw = await readFile4(this.filePath, "utf8");
2372
2696
  const parsed = JSON.parse(raw);
2373
2697
  return Array.isArray(parsed) ? parsed : [];
2374
2698
  } catch {
@@ -2377,10 +2701,10 @@ var FilePipelineRunStore = class {
2377
2701
  }
2378
2702
  async write(runs) {
2379
2703
  const tmp = `${this.filePath}.tmp`;
2380
- await mkdir3(dirname3(this.filePath), { recursive: true });
2381
- await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
2704
+ await mkdir4(dirname4(this.filePath), { recursive: true });
2705
+ await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
2382
2706
  `, "utf8");
2383
- await rename3(tmp, this.filePath);
2707
+ await rename4(tmp, this.filePath);
2384
2708
  }
2385
2709
  };
2386
2710
 
@@ -2392,6 +2716,7 @@ function buildPipelineEngine(config, log = logger) {
2392
2716
  agents,
2393
2717
  roles,
2394
2718
  store: new FileTaskStore(),
2719
+ ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
2395
2720
  log
2396
2721
  });
2397
2722
  return new PipelineEngine({
@@ -2493,6 +2818,59 @@ function findPipeline(config, id) {
2493
2818
  return def;
2494
2819
  }
2495
2820
 
2821
+ // src/cli/commands/memory.ts
2822
+ function registerMemoryCommands(program) {
2823
+ const memory = program.command("memory").description("Inspect and manage persistent project memory (\xA719\u2013\xA720).");
2824
+ memory.command("status").description("Show the active memory provider and how much is stored.").action(async () => {
2825
+ const config = loadConfig();
2826
+ const configured = config.project.memory?.provider ?? "local (default \u2014 add a `memory:` section to opt in)";
2827
+ const svc = new MemoryService(createMemoryProvider(config.project.memory));
2828
+ const entries = await svc.providerRef.query({ limit: 1e4 });
2829
+ console.log(`provider: ${configured}`);
2830
+ console.log(`entries: ${entries.length}`);
2831
+ const scopes = /* @__PURE__ */ new Map();
2832
+ for (const entry2 of entries) {
2833
+ scopes.set(entry2.scope, (scopes.get(entry2.scope) ?? 0) + 1);
2834
+ }
2835
+ for (const [scope, count] of [...scopes.entries()].sort()) {
2836
+ console.log(` ${scope}: ${count}`);
2837
+ }
2838
+ });
2839
+ memory.command("query <text>").description("Search memory for entries relevant to the text.").option("-s, --scope <scope>", "Restrict to a scope: project, task, role or knowledge.").option("-n, --limit <count>", "Maximum entries to return.", (value) => Number(value), 10).action(async (text, options) => {
2840
+ const config = loadConfig();
2841
+ const svc = new MemoryService(createMemoryProvider(config.project.memory));
2842
+ const entries = await svc.providerRef.query({
2843
+ text,
2844
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2845
+ limit: options.limit
2846
+ });
2847
+ if (entries.length === 0) {
2848
+ console.log("No relevant memory found.");
2849
+ return;
2850
+ }
2851
+ for (const entry2 of entries) {
2852
+ console.log(`[${entry2.scope}] ${entry2.key}`);
2853
+ console.log(` ${truncate(entry2.content.replace(/\n/g, " "), 120)}`);
2854
+ }
2855
+ });
2856
+ memory.command("write <text>").description("Write an entry to memory.").option("-s, --scope <scope>", "Memory scope: project, task, role or knowledge.", "project").option(
2857
+ "-t, --tags <tags>",
2858
+ "Comma-separated tags.",
2859
+ (value) => value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0)
2860
+ ).option("-k, --key <key>", "Entry key (defaults to a generated note key).").action(async (text, options) => {
2861
+ const config = loadConfig();
2862
+ const svc = new MemoryService(createMemoryProvider(config.project.memory));
2863
+ const key = options.key ?? `note:${Date.now().toString(36)}`;
2864
+ await svc.providerRef.write({
2865
+ key,
2866
+ scope: options.scope,
2867
+ content: text,
2868
+ ...options.tags !== void 0 && options.tags.length > 0 ? { tags: options.tags } : {}
2869
+ });
2870
+ console.log(`Stored memory entry "${key}" (${options.scope}).`);
2871
+ });
2872
+ }
2873
+
2496
2874
  // src/cli/index.ts
2497
2875
  function createProgram() {
2498
2876
  const program = new Command();
@@ -2508,6 +2886,7 @@ function createProgram() {
2508
2886
  registerReportCommands(program);
2509
2887
  registerModelsCommand(program);
2510
2888
  registerPipelineCommands(program);
2889
+ registerMemoryCommands(program);
2511
2890
  return program;
2512
2891
  }
2513
2892
  async function run(argv) {