@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 +22 -3
- package/dist/cli/index.js +398 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +143 -11
- package/dist/index.js +350 -20
- 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.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
|
-
|
|
|
194
|
-
| v0.8.0 | Remembera
|
|
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.
|
|
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({
|
|
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 ? {
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
1488
|
-
roles
|
|
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
|
|
1792
|
-
import { dirname as
|
|
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
|
|
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
|
|
1866
|
-
await
|
|
2189
|
+
await mkdir3(dirname3(this.filePath), { recursive: true });
|
|
2190
|
+
await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1867
2191
|
`, "utf8");
|
|
1868
|
-
await
|
|
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
|
|
2333
|
-
import { dirname as
|
|
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
|
|
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
|
|
2381
|
-
await
|
|
2704
|
+
await mkdir4(dirname4(this.filePath), { recursive: true });
|
|
2705
|
+
await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
|
|
2382
2706
|
`, "utf8");
|
|
2383
|
-
await
|
|
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) {
|