@amoree-code/ocean 1.1.0 → 1.2.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/node_modules/@ocean/brain/package.json +1 -1
- package/node_modules/@ocean/context/package.json +1 -1
- package/node_modules/@ocean/core/package.json +1 -1
- package/node_modules/@ocean/loop/package.json +1 -1
- package/node_modules/@ocean/market/package.json +1 -1
- package/node_modules/@ocean/services/package.json +1 -1
- package/ocean-marketplace.json +28 -28
- package/package.json +7 -7
- package/node_modules/@ocean/brain/dist/memory/signal-detection.js +0 -66
- package/node_modules/@ocean/market/dist/security/sandbox.js +0 -74
- package/node_modules/@ocean/market/dist/skills/skill-curation.js +0 -123
- package/node_modules/@ocean/market/dist/skills/task-observer.js +0 -165
package/ocean-marketplace.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
{
|
|
6
6
|
"kind": "skill",
|
|
7
7
|
"name": "dev.ocean.skills.business-logic",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.2.0",
|
|
9
9
|
"engines": {
|
|
10
|
-
"ocean": ">=1.
|
|
10
|
+
"ocean": ">=1.2.0"
|
|
11
11
|
},
|
|
12
12
|
"source": {
|
|
13
13
|
"type": "path",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
{
|
|
24
24
|
"kind": "skill",
|
|
25
25
|
"name": "dev.ocean.skills.catch-up",
|
|
26
|
-
"version": "1.
|
|
26
|
+
"version": "1.2.0",
|
|
27
27
|
"engines": {
|
|
28
|
-
"ocean": ">=1.
|
|
28
|
+
"ocean": ">=1.2.0"
|
|
29
29
|
},
|
|
30
30
|
"source": {
|
|
31
31
|
"type": "path",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
{
|
|
42
42
|
"kind": "skill",
|
|
43
43
|
"name": "dev.ocean.skills.core-thinking",
|
|
44
|
-
"version": "1.
|
|
44
|
+
"version": "1.2.0",
|
|
45
45
|
"engines": {
|
|
46
|
-
"ocean": ">=1.
|
|
46
|
+
"ocean": ">=1.2.0"
|
|
47
47
|
},
|
|
48
48
|
"source": {
|
|
49
49
|
"type": "path",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
{
|
|
60
60
|
"kind": "skill",
|
|
61
61
|
"name": "dev.ocean.skills.design-thinking",
|
|
62
|
-
"version": "1.
|
|
62
|
+
"version": "1.2.0",
|
|
63
63
|
"engines": {
|
|
64
|
-
"ocean": ">=1.
|
|
64
|
+
"ocean": ">=1.2.0"
|
|
65
65
|
},
|
|
66
66
|
"source": {
|
|
67
67
|
"type": "path",
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
{
|
|
78
78
|
"kind": "skill",
|
|
79
79
|
"name": "dev.ocean.skills.graft",
|
|
80
|
-
"version": "1.
|
|
80
|
+
"version": "1.2.0",
|
|
81
81
|
"engines": {
|
|
82
|
-
"ocean": ">=1.
|
|
82
|
+
"ocean": ">=1.2.0"
|
|
83
83
|
},
|
|
84
84
|
"source": {
|
|
85
85
|
"type": "path",
|
|
@@ -95,9 +95,9 @@
|
|
|
95
95
|
{
|
|
96
96
|
"kind": "skill",
|
|
97
97
|
"name": "dev.ocean.skills.loop",
|
|
98
|
-
"version": "1.
|
|
98
|
+
"version": "1.2.0",
|
|
99
99
|
"engines": {
|
|
100
|
-
"ocean": ">=1.
|
|
100
|
+
"ocean": ">=1.2.0"
|
|
101
101
|
},
|
|
102
102
|
"source": {
|
|
103
103
|
"type": "path",
|
|
@@ -113,9 +113,9 @@
|
|
|
113
113
|
{
|
|
114
114
|
"kind": "skill",
|
|
115
115
|
"name": "dev.ocean.skills.session-handoff",
|
|
116
|
-
"version": "1.
|
|
116
|
+
"version": "1.2.0",
|
|
117
117
|
"engines": {
|
|
118
|
-
"ocean": ">=1.
|
|
118
|
+
"ocean": ">=1.2.0"
|
|
119
119
|
},
|
|
120
120
|
"source": {
|
|
121
121
|
"type": "path",
|
|
@@ -131,9 +131,9 @@
|
|
|
131
131
|
{
|
|
132
132
|
"kind": "skill",
|
|
133
133
|
"name": "dev.ocean.skills.use-browser",
|
|
134
|
-
"version": "1.
|
|
134
|
+
"version": "1.2.0",
|
|
135
135
|
"engines": {
|
|
136
|
-
"ocean": ">=1.
|
|
136
|
+
"ocean": ">=1.2.0"
|
|
137
137
|
},
|
|
138
138
|
"source": {
|
|
139
139
|
"type": "path",
|
|
@@ -149,9 +149,9 @@
|
|
|
149
149
|
{
|
|
150
150
|
"kind": "skill",
|
|
151
151
|
"name": "dev.ocean.skills.verification",
|
|
152
|
-
"version": "1.
|
|
152
|
+
"version": "1.2.0",
|
|
153
153
|
"engines": {
|
|
154
|
-
"ocean": ">=1.
|
|
154
|
+
"ocean": ">=1.2.0"
|
|
155
155
|
},
|
|
156
156
|
"source": {
|
|
157
157
|
"type": "path",
|
|
@@ -183,9 +183,9 @@
|
|
|
183
183
|
"npx"
|
|
184
184
|
]
|
|
185
185
|
},
|
|
186
|
-
"version": "1.
|
|
186
|
+
"version": "1.2.0",
|
|
187
187
|
"engines": {
|
|
188
|
-
"ocean": ">=1.
|
|
188
|
+
"ocean": ">=1.2.0"
|
|
189
189
|
},
|
|
190
190
|
"source": {
|
|
191
191
|
"type": "path",
|
|
@@ -232,9 +232,9 @@
|
|
|
232
232
|
"kind": "mcp",
|
|
233
233
|
"name": "dev.ocean/mcp-gateway",
|
|
234
234
|
"description": "One MCP endpoint for every registered server: Ocean's own and those installed from the marketplace.",
|
|
235
|
-
"version": "1.
|
|
235
|
+
"version": "1.2.0",
|
|
236
236
|
"engines": {
|
|
237
|
-
"ocean": ">=1.
|
|
237
|
+
"ocean": ">=1.2.0"
|
|
238
238
|
},
|
|
239
239
|
"source": {
|
|
240
240
|
"type": "path",
|
|
@@ -276,9 +276,9 @@
|
|
|
276
276
|
"kind": "mcp",
|
|
277
277
|
"name": "dev.ocean/obsidian",
|
|
278
278
|
"description": "Ocean's Obsidian vault bridge over MCP.",
|
|
279
|
-
"version": "1.
|
|
279
|
+
"version": "1.2.0",
|
|
280
280
|
"engines": {
|
|
281
|
-
"ocean": ">=1.
|
|
281
|
+
"ocean": ">=1.2.0"
|
|
282
282
|
},
|
|
283
283
|
"source": {
|
|
284
284
|
"type": "path",
|
|
@@ -320,9 +320,9 @@
|
|
|
320
320
|
"kind": "mcp",
|
|
321
321
|
"name": "dev.ocean/ocean",
|
|
322
322
|
"description": "Ocean's own MCP server: workspace status, tasks, handoffs, sessions and the brain.",
|
|
323
|
-
"version": "1.
|
|
323
|
+
"version": "1.2.0",
|
|
324
324
|
"engines": {
|
|
325
|
-
"ocean": ">=1.
|
|
325
|
+
"ocean": ">=1.2.0"
|
|
326
326
|
},
|
|
327
327
|
"source": {
|
|
328
328
|
"type": "path",
|
|
@@ -374,9 +374,9 @@
|
|
|
374
374
|
"npx"
|
|
375
375
|
]
|
|
376
376
|
},
|
|
377
|
-
"version": "1.
|
|
377
|
+
"version": "1.2.0",
|
|
378
378
|
"engines": {
|
|
379
|
-
"ocean": ">=1.
|
|
379
|
+
"ocean": ">=1.2.0"
|
|
380
380
|
},
|
|
381
381
|
"source": {
|
|
382
382
|
"type": "path",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amoree-code/ocean",
|
|
3
3
|
"description": "Local-first memory, tasks and session capture shared by every AI coding client: one private workspace for Claude, Codex, Gemini and more.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "AMEER ABDULKAREEM JABBAR LAMI",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@langchain/core": "1.2.14",
|
|
46
46
|
"@langchain/langgraph": "1.4.19",
|
|
47
47
|
"@modelcontextprotocol/sdk": "1.32.0",
|
|
48
|
-
"@ocean/brain": "1.
|
|
49
|
-
"@ocean/context": "1.
|
|
50
|
-
"@ocean/core": "1.
|
|
51
|
-
"@ocean/loop": "1.
|
|
52
|
-
"@ocean/market": "1.
|
|
53
|
-
"@ocean/services": "1.
|
|
48
|
+
"@ocean/brain": "1.2.0",
|
|
49
|
+
"@ocean/context": "1.2.0",
|
|
50
|
+
"@ocean/core": "1.2.0",
|
|
51
|
+
"@ocean/loop": "1.2.0",
|
|
52
|
+
"@ocean/market": "1.2.0",
|
|
53
|
+
"@ocean/services": "1.2.0",
|
|
54
54
|
"playwright-core": "^1.48.0",
|
|
55
55
|
"sqlite-vec": "0.1.9",
|
|
56
56
|
"zod": "^4.6.5"
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { redactRuntimeText } from "@ocean/core/domain/redaction/redaction.js";
|
|
2
|
-
const CORRECTION_PATTERN = /\bno,|\bactually,|\bnot that\b|\binstead of\b|\bshould use\b|\bthat'?s wrong\b/i;
|
|
3
|
-
function userInputTexts(events) {
|
|
4
|
-
return events
|
|
5
|
-
.filter((event) => event.type === "user_input")
|
|
6
|
-
.map((event) => ({
|
|
7
|
-
id: event.eventId,
|
|
8
|
-
text: redactRuntimeText(event.data).replace(/\s+/g, " ").trim(),
|
|
9
|
-
}))
|
|
10
|
-
.filter((event) => event.text.length > 12)
|
|
11
|
-
.slice(0, 80);
|
|
12
|
-
}
|
|
13
|
-
// True when the user pushed back in this session ("no, use X", "that's wrong").
|
|
14
|
-
export function hasUserCorrection(events) {
|
|
15
|
-
return userInputTexts(events).some((event) => CORRECTION_PATTERN.test(event.text));
|
|
16
|
-
}
|
|
17
|
-
// What a completed, proven session is worth remembering. Pure: reads events, writes nothing.
|
|
18
|
-
export function detectSessionSignals(events) {
|
|
19
|
-
const proven = events.filter((event) => event.type === "evidence" && /"result"\s*:\s*"proven"/.test(event.data));
|
|
20
|
-
if (!proven.length)
|
|
21
|
-
return [];
|
|
22
|
-
const outputs = events
|
|
23
|
-
.filter((event) => ["provider_output", "text", "json"].includes(event.type))
|
|
24
|
-
.map((event) => ({
|
|
25
|
-
id: event.eventId,
|
|
26
|
-
text: redactRuntimeText(event.data).replace(/\s+/g, " ").trim(),
|
|
27
|
-
}))
|
|
28
|
-
.filter((event) => event.text.length > 12)
|
|
29
|
-
.slice(0, 80);
|
|
30
|
-
const userInputs = userInputTexts(events);
|
|
31
|
-
const candidates = [];
|
|
32
|
-
const decisions = outputs.filter((event) => /\bdecision\s*:/i.test(event.text));
|
|
33
|
-
if (decisions.length)
|
|
34
|
-
candidates.push({
|
|
35
|
-
signalType: "explicit-decision",
|
|
36
|
-
summary: decisions[0].text.slice(0, 500),
|
|
37
|
-
refs: decisions.slice(0, 4).map((event) => `event:${event.id}`),
|
|
38
|
-
confidence: 0.9,
|
|
39
|
-
});
|
|
40
|
-
const counts = new Map();
|
|
41
|
-
for (const event of outputs) {
|
|
42
|
-
const normalized = event.text.toLocaleLowerCase();
|
|
43
|
-
const current = counts.get(normalized) ?? { count: 0, refs: [] };
|
|
44
|
-
current.count += 1;
|
|
45
|
-
current.refs.push(`event:${event.id}`);
|
|
46
|
-
counts.set(normalized, current);
|
|
47
|
-
}
|
|
48
|
-
const repeated = [...counts.entries()].find(([, value]) => value.count >= 2);
|
|
49
|
-
if (repeated)
|
|
50
|
-
candidates.push({
|
|
51
|
-
signalType: "repeated-procedure",
|
|
52
|
-
summary: repeated[0].slice(0, 500),
|
|
53
|
-
refs: repeated[1].refs.slice(0, 4),
|
|
54
|
-
confidence: 0.8,
|
|
55
|
-
});
|
|
56
|
-
const corrections = userInputs.filter((event) => CORRECTION_PATTERN.test(event.text));
|
|
57
|
-
if (corrections.length)
|
|
58
|
-
candidates.push({
|
|
59
|
-
signalType: "repeated-correction",
|
|
60
|
-
summary: corrections[0].text.slice(0, 500),
|
|
61
|
-
refs: corrections.slice(0, 4).map((event) => `event:${event.id}`),
|
|
62
|
-
confidence: 0.7,
|
|
63
|
-
});
|
|
64
|
-
return candidates;
|
|
65
|
-
}
|
|
66
|
-
//# sourceMappingURL=signal-detection.js.map
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { realDeepestExisting } from "@ocean/core/shared/paths.js";
|
|
4
|
-
// Where a package runner (npx, uvx) must be able to write to work at all: its caches and the
|
|
5
|
-
// temp dir. Everything else a capability writes has to be declared.
|
|
6
|
-
export function runtimeWritablePaths(home = os.homedir()) {
|
|
7
|
-
return [
|
|
8
|
-
os.tmpdir(),
|
|
9
|
-
path.join(home, ".npm"),
|
|
10
|
-
path.join(home, ".cache"),
|
|
11
|
-
path.join(home, "Library", "Caches"),
|
|
12
|
-
path.join(home, ".local", "share", "uv"),
|
|
13
|
-
];
|
|
14
|
-
}
|
|
15
|
-
// Reads stay open for system libraries, but these never are unless declared under fs.read.
|
|
16
|
-
export function secretPaths(home) {
|
|
17
|
-
return [
|
|
18
|
-
".ssh",
|
|
19
|
-
".aws",
|
|
20
|
-
".gnupg",
|
|
21
|
-
".kube",
|
|
22
|
-
".docker",
|
|
23
|
-
".netrc",
|
|
24
|
-
".npmrc",
|
|
25
|
-
".config/gh",
|
|
26
|
-
".config/gcloud",
|
|
27
|
-
"Library/Keychains",
|
|
28
|
-
].map((entry) => path.join(home, entry));
|
|
29
|
-
}
|
|
30
|
-
const sbplString = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
31
|
-
// macOS Seatbelt profile: reads stay open (runtimes need system libraries) except secret
|
|
32
|
-
// locations, writes are denied
|
|
33
|
-
// outside the declared and runtime paths, and the network is denied unless any host was
|
|
34
|
-
// declared. Seatbelt cannot filter by hostname, so a declared host opens outbound network;
|
|
35
|
-
// remote endpoints are still checked per host by the launcher.
|
|
36
|
-
export function seatbeltProfile(policy, extraWritable) {
|
|
37
|
-
const writable = [
|
|
38
|
-
...new Set([...policy.writablePaths(), ...extraWritable].map(realDeepestExisting)),
|
|
39
|
-
];
|
|
40
|
-
const lines = [
|
|
41
|
-
"(version 1)",
|
|
42
|
-
"(allow default)",
|
|
43
|
-
"(deny file-write*)",
|
|
44
|
-
`(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/dtracehelper")${writable
|
|
45
|
-
.map((entry) => ` (subpath ${sbplString(entry)})`)
|
|
46
|
-
.join("")})`,
|
|
47
|
-
];
|
|
48
|
-
const secrets = secretPaths(policy.home)
|
|
49
|
-
.filter((entry) => !policy.allows({ type: "fs.read", target: entry }))
|
|
50
|
-
.map(realDeepestExisting);
|
|
51
|
-
if (secrets.length)
|
|
52
|
-
lines.push(`(deny file-read* file-write*${secrets.map((entry) => ` (subpath ${sbplString(entry)})`).join("")})`);
|
|
53
|
-
if (!policy.permissions.network.length)
|
|
54
|
-
lines.push("(deny network*)");
|
|
55
|
-
return lines.join("\n");
|
|
56
|
-
}
|
|
57
|
-
export function sandboxed(policy, target, options) {
|
|
58
|
-
const platform = options.platform ?? process.platform;
|
|
59
|
-
if (platform === "darwin")
|
|
60
|
-
return {
|
|
61
|
-
command: "/usr/bin/sandbox-exec",
|
|
62
|
-
args: [
|
|
63
|
-
"-p",
|
|
64
|
-
seatbeltProfile(policy, options.extraWritable),
|
|
65
|
-
target.command,
|
|
66
|
-
...target.args,
|
|
67
|
-
],
|
|
68
|
-
sandbox: "sandbox-exec",
|
|
69
|
-
};
|
|
70
|
-
if (!options.allowUnsandboxed)
|
|
71
|
-
throw new Error(`No process sandbox is available on ${platform}; refusing to launch ${policy.capability} unsandboxed`);
|
|
72
|
-
return { ...target, sandbox: "none" };
|
|
73
|
-
}
|
|
74
|
-
//# sourceMappingURL=sandbox.js.map
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { redactRuntimeText } from "@ocean/core/domain/redaction/redaction.js";
|
|
4
|
-
import { truncateUtf8 } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
-
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
6
|
-
const file = () => oceanPath(SYSTEM_DIR, "skills", "candidates.json");
|
|
7
|
-
async function load() {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(await readFile(file(), "utf8"));
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
12
|
-
if (error.code === "ENOENT")
|
|
13
|
-
return [];
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
async function save(items) {
|
|
18
|
-
await mkdir(path.dirname(file()), { recursive: true });
|
|
19
|
-
await writeFile(file(), `${JSON.stringify(items, null, 2)}\n`, {
|
|
20
|
-
mode: 0o600,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
export async function addSkillCandidate(candidate) {
|
|
24
|
-
if (!candidate.id ||
|
|
25
|
-
!candidate.name ||
|
|
26
|
-
!candidate.instructions ||
|
|
27
|
-
candidate.instructions.length > 32_000 ||
|
|
28
|
-
/(api[_ -]?key|token|password|secret)\s*[:=]/i.test(candidate.instructions))
|
|
29
|
-
throw new Error("Invalid or sensitive skill candidate");
|
|
30
|
-
const items = await load();
|
|
31
|
-
if (items.some((item) => item.id === candidate.id || item.name === candidate.name))
|
|
32
|
-
throw new Error("Duplicate skill candidate");
|
|
33
|
-
const result = {
|
|
34
|
-
...candidate,
|
|
35
|
-
version: "1.0.0",
|
|
36
|
-
status: "candidate",
|
|
37
|
-
verification: "unverified",
|
|
38
|
-
createdAt: new Date().toISOString(),
|
|
39
|
-
};
|
|
40
|
-
await save([...items, result]);
|
|
41
|
-
return result;
|
|
42
|
-
}
|
|
43
|
-
export async function listSkillCandidates() {
|
|
44
|
-
return load();
|
|
45
|
-
}
|
|
46
|
-
export async function loadPromotedSkills(prompt, maxBytes = 32_000) {
|
|
47
|
-
const normalizedPrompt = prompt.toLocaleLowerCase();
|
|
48
|
-
const candidates = (await load()).filter((candidate) => candidate.status === "promoted" &&
|
|
49
|
-
candidate.verification === "owner-reviewed");
|
|
50
|
-
const selected = [];
|
|
51
|
-
let bytes = 0;
|
|
52
|
-
for (const candidate of candidates) {
|
|
53
|
-
const aliases = [candidate.id, candidate.name].map((value) => value.toLocaleLowerCase().replaceAll("-", " "));
|
|
54
|
-
if (!aliases.some((alias) => normalizedPrompt.includes(alias)))
|
|
55
|
-
continue;
|
|
56
|
-
const remaining = maxBytes - bytes;
|
|
57
|
-
if (remaining <= 0)
|
|
58
|
-
break;
|
|
59
|
-
const size = Buffer.byteLength(candidate.instructions);
|
|
60
|
-
if (size <= remaining) {
|
|
61
|
-
selected.push({ ...candidate, truncated: false });
|
|
62
|
-
bytes += size;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
// A usable pointer: the store file and the command that prints it. The store is not
|
|
66
|
-
// granted to restricted providers because it also holds unreviewed candidates.
|
|
67
|
-
const marker = `\n[truncated to the run budget; full text: entry id "${candidate.id}" in \`ocean skill list\` or ${file()}]`;
|
|
68
|
-
const markerBytes = Buffer.byteLength(marker);
|
|
69
|
-
if (remaining <= markerBytes)
|
|
70
|
-
break;
|
|
71
|
-
const instructions = `${truncateUtf8(candidate.instructions, remaining - markerBytes)}${marker}`;
|
|
72
|
-
selected.push({ ...candidate, instructions, truncated: true });
|
|
73
|
-
bytes += Buffer.byteLength(instructions);
|
|
74
|
-
}
|
|
75
|
-
return selected;
|
|
76
|
-
}
|
|
77
|
-
export async function learnSkillFromSession(sessionId, openStore) {
|
|
78
|
-
const store = await openStore();
|
|
79
|
-
try {
|
|
80
|
-
const session = store.get(sessionId);
|
|
81
|
-
if (!session)
|
|
82
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
83
|
-
if (session.status !== "completed")
|
|
84
|
-
throw new Error(`Only completed sessions can teach a skill: ${session.status}`);
|
|
85
|
-
if (!store
|
|
86
|
-
.listEvents(sessionId)
|
|
87
|
-
.some((event) => event.type === "evidence" &&
|
|
88
|
-
/"result"\s*:\s*"proven"/.test(event.data)))
|
|
89
|
-
throw new Error("Session lacks independently recorded successful evidence");
|
|
90
|
-
const instructions = store
|
|
91
|
-
.listEvents(sessionId)
|
|
92
|
-
.filter((event) => event.type === "provider_output" ||
|
|
93
|
-
event.type === "text" ||
|
|
94
|
-
event.type === "json")
|
|
95
|
-
.map((event) => redactRuntimeText(event.data).trim())
|
|
96
|
-
.filter(Boolean)
|
|
97
|
-
.join("\n\n")
|
|
98
|
-
.slice(0, 32_000);
|
|
99
|
-
if (!instructions)
|
|
100
|
-
throw new Error("Session has no provider output to learn from");
|
|
101
|
-
const id = `learned-${session.provider}-${sessionId.slice(0, 8)}`;
|
|
102
|
-
return addSkillCandidate({
|
|
103
|
-
id,
|
|
104
|
-
name: `Learned ${session.provider} skill`,
|
|
105
|
-
instructions,
|
|
106
|
-
sourceSessionId: sessionId,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
store.close();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
export async function reviewSkillCandidate(id, status) {
|
|
114
|
-
const items = await load();
|
|
115
|
-
const item = items.find((candidate) => candidate.id === id);
|
|
116
|
-
if (!item)
|
|
117
|
-
throw new Error(`Skill candidate not found: ${id}`);
|
|
118
|
-
item.status = status;
|
|
119
|
-
item.verification = status === "promoted" ? "owner-reviewed" : "unverified";
|
|
120
|
-
await save(items);
|
|
121
|
-
return item;
|
|
122
|
-
}
|
|
123
|
-
//# sourceMappingURL=skill-curation.js.map
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { redactRuntimeText } from "@ocean/core/domain/redaction/redaction.js";
|
|
5
|
-
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
6
|
-
import { addSkillCandidate } from "./skill-curation.js";
|
|
7
|
-
const observationsFile = () => oceanPath(SYSTEM_DIR, "skills", "observations.json");
|
|
8
|
-
async function load() {
|
|
9
|
-
try {
|
|
10
|
-
return JSON.parse(await readFile(observationsFile(), "utf8"));
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
if (error.code === "ENOENT")
|
|
14
|
-
return [];
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
async function save(items) {
|
|
19
|
-
await mkdir(path.dirname(observationsFile()), { recursive: true });
|
|
20
|
-
await writeFile(observationsFile(), `${JSON.stringify(items.slice(-200), null, 2)}\n`, { mode: 0o600 });
|
|
21
|
-
}
|
|
22
|
-
function observationId(sessionId, signalType, summary) {
|
|
23
|
-
return `obs-${createHash("sha256").update(`${sessionId}:${signalType}:${summary}`).digest("hex").slice(0, 16)}`;
|
|
24
|
-
}
|
|
25
|
-
const CORRECTION_PATTERN = /\bno,|\bactually,|\bnot that\b|\binstead of\b|\bshould use\b|\bthat'?s wrong\b/i;
|
|
26
|
-
function userInputTexts(events) {
|
|
27
|
-
return events
|
|
28
|
-
.filter((event) => event.type === "user_input")
|
|
29
|
-
.map((event) => ({
|
|
30
|
-
id: event.eventId,
|
|
31
|
-
text: redactRuntimeText(event.data).replace(/\s+/g, " ").trim(),
|
|
32
|
-
}))
|
|
33
|
-
.filter((event) => event.text.length > 12)
|
|
34
|
-
.slice(0, 80);
|
|
35
|
-
}
|
|
36
|
-
// True when the user pushed back in this session ("no, use X", "that's wrong").
|
|
37
|
-
export function hasUserCorrection(events) {
|
|
38
|
-
return userInputTexts(events).some((event) => CORRECTION_PATTERN.test(event.text));
|
|
39
|
-
}
|
|
40
|
-
// What a completed, proven session is worth remembering. Pure: reads events, writes nothing.
|
|
41
|
-
export function detectSessionSignals(events) {
|
|
42
|
-
const proven = events.filter((event) => event.type === "evidence" && /"result"\s*:\s*"proven"/.test(event.data));
|
|
43
|
-
if (!proven.length)
|
|
44
|
-
return [];
|
|
45
|
-
const outputs = events
|
|
46
|
-
.filter((event) => ["provider_output", "text", "json"].includes(event.type))
|
|
47
|
-
.map((event) => ({
|
|
48
|
-
id: event.eventId,
|
|
49
|
-
text: redactRuntimeText(event.data).replace(/\s+/g, " ").trim(),
|
|
50
|
-
}))
|
|
51
|
-
.filter((event) => event.text.length > 12)
|
|
52
|
-
.slice(0, 80);
|
|
53
|
-
const userInputs = userInputTexts(events);
|
|
54
|
-
const candidates = [];
|
|
55
|
-
const decisions = outputs.filter((event) => /\bdecision\s*:/i.test(event.text));
|
|
56
|
-
if (decisions.length)
|
|
57
|
-
candidates.push({
|
|
58
|
-
signalType: "explicit-decision",
|
|
59
|
-
summary: decisions[0].text.slice(0, 500),
|
|
60
|
-
refs: decisions.slice(0, 4).map((event) => `event:${event.id}`),
|
|
61
|
-
confidence: 0.9,
|
|
62
|
-
});
|
|
63
|
-
const counts = new Map();
|
|
64
|
-
for (const event of outputs) {
|
|
65
|
-
const normalized = event.text.toLocaleLowerCase();
|
|
66
|
-
const current = counts.get(normalized) ?? { count: 0, refs: [] };
|
|
67
|
-
current.count += 1;
|
|
68
|
-
current.refs.push(`event:${event.id}`);
|
|
69
|
-
counts.set(normalized, current);
|
|
70
|
-
}
|
|
71
|
-
const repeated = [...counts.entries()].find(([, value]) => value.count >= 2);
|
|
72
|
-
if (repeated)
|
|
73
|
-
candidates.push({
|
|
74
|
-
signalType: "repeated-procedure",
|
|
75
|
-
summary: repeated[0].slice(0, 500),
|
|
76
|
-
refs: repeated[1].refs.slice(0, 4),
|
|
77
|
-
confidence: 0.8,
|
|
78
|
-
});
|
|
79
|
-
const corrections = userInputs.filter((event) => CORRECTION_PATTERN.test(event.text));
|
|
80
|
-
if (corrections.length)
|
|
81
|
-
candidates.push({
|
|
82
|
-
signalType: "repeated-correction",
|
|
83
|
-
summary: corrections[0].text.slice(0, 500),
|
|
84
|
-
refs: corrections.slice(0, 4).map((event) => `event:${event.id}`),
|
|
85
|
-
confidence: 0.7,
|
|
86
|
-
});
|
|
87
|
-
return candidates;
|
|
88
|
-
}
|
|
89
|
-
export async function observeSessionWithStore(store, sessionId) {
|
|
90
|
-
const session = store.get(sessionId);
|
|
91
|
-
if (!session)
|
|
92
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
93
|
-
if (session.status !== "completed")
|
|
94
|
-
return [];
|
|
95
|
-
const events = store.listEvents(sessionId);
|
|
96
|
-
const candidates = detectSessionSignals(events);
|
|
97
|
-
const items = await load();
|
|
98
|
-
const created = [];
|
|
99
|
-
for (const candidate of candidates.slice(0, 4)) {
|
|
100
|
-
const id = observationId(sessionId, candidate.signalType, candidate.summary);
|
|
101
|
-
if (items.some((item) => item.observationId === id))
|
|
102
|
-
continue;
|
|
103
|
-
created.push({
|
|
104
|
-
observationId: id,
|
|
105
|
-
sourceSessionId: sessionId,
|
|
106
|
-
taskId: session.taskId,
|
|
107
|
-
profileId: session.profile,
|
|
108
|
-
signalType: candidate.signalType,
|
|
109
|
-
summary: candidate.summary,
|
|
110
|
-
evidenceRefs: candidate.refs,
|
|
111
|
-
confidence: candidate.confidence,
|
|
112
|
-
status: "observed",
|
|
113
|
-
createdAt: new Date().toISOString(),
|
|
114
|
-
reviewedAt: null,
|
|
115
|
-
skillCandidateId: null,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (created.length)
|
|
119
|
-
await save([...items, ...created]);
|
|
120
|
-
return created;
|
|
121
|
-
}
|
|
122
|
-
export async function observeSession(sessionId, openStore) {
|
|
123
|
-
const store = await openStore();
|
|
124
|
-
try {
|
|
125
|
-
return await observeSessionWithStore(store, sessionId);
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
store.close();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
export async function listObservations() {
|
|
132
|
-
return load();
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Approving an observation creates the skill candidate it was promising —
|
|
136
|
-
* reviewObservation used to only flip a status flag with nothing downstream.
|
|
137
|
-
* A duplicate or invalid candidate (see addSkillCandidate) leaves the
|
|
138
|
-
* approval standing but skillCandidateId null; the summary is short and
|
|
139
|
-
* redacted, so this should be rare.
|
|
140
|
-
*/
|
|
141
|
-
export async function reviewObservation(observationIdValue, status) {
|
|
142
|
-
const items = await load();
|
|
143
|
-
const item = items.find((candidate) => candidate.observationId === observationIdValue);
|
|
144
|
-
if (!item)
|
|
145
|
-
throw new Error(`Observation not found: ${observationIdValue}`);
|
|
146
|
-
item.status = status;
|
|
147
|
-
item.reviewedAt = new Date().toISOString();
|
|
148
|
-
if (status === "approved" && !item.skillCandidateId) {
|
|
149
|
-
try {
|
|
150
|
-
const candidate = await addSkillCandidate({
|
|
151
|
-
id: `learned-${item.observationId.slice(4, 16)}`,
|
|
152
|
-
name: `Learned: ${item.signalType} (${item.observationId.slice(4, 10)})`,
|
|
153
|
-
instructions: item.summary,
|
|
154
|
-
sourceSessionId: item.sourceSessionId,
|
|
155
|
-
});
|
|
156
|
-
item.skillCandidateId = candidate.id;
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Duplicate id/name or invalid content — approval stands, no candidate.
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
await save(items);
|
|
163
|
-
return item;
|
|
164
|
-
}
|
|
165
|
-
//# sourceMappingURL=task-observer.js.map
|