@amoree-code/ocean 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- package/templates/workspace/sessions/.gitkeep +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { safeJsonParse } from "@ocean/core/shared/fs-utils.js";
|
|
3
|
+
import { detectSessionSignals, hasUserCorrection, } from "@ocean/market/skills/signal-detection.js";
|
|
4
|
+
import { addLesson, confirmLesson, contradictLesson, LessonRejectedError, readLesson, setLessonStatus, } from "./lessons.js";
|
|
5
|
+
// What a closed session teaches the lessons store (T-230 P2). Scoring reads only signals a
|
|
6
|
+
// session recorded itself — its exit, its evidence events — never the model's own claims.
|
|
7
|
+
export const PROMOTION_HITS = 3;
|
|
8
|
+
const MAX_NEW_LESSONS = 4;
|
|
9
|
+
// verified = completed, exited 0, at least one proven evidence event, and none unproven.
|
|
10
|
+
export function scoreSession(session, events) {
|
|
11
|
+
const exit = [...events].reverse().find((e) => e.type === "process_exit");
|
|
12
|
+
const exitCode = exit
|
|
13
|
+
? Number(safeJsonParse(exit.data)?.exitCode ?? 1)
|
|
14
|
+
: session.status === "completed"
|
|
15
|
+
? 0
|
|
16
|
+
: 1;
|
|
17
|
+
let provenCount = 0;
|
|
18
|
+
let unprovenCount = 0;
|
|
19
|
+
for (const event of events) {
|
|
20
|
+
if (event.type !== "evidence")
|
|
21
|
+
continue;
|
|
22
|
+
if (safeJsonParse(event.data)?.result === "proven")
|
|
23
|
+
provenCount += 1;
|
|
24
|
+
else
|
|
25
|
+
unprovenCount += 1;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
verified: session.status === "completed" &&
|
|
29
|
+
exitCode === 0 &&
|
|
30
|
+
provenCount > 0 &&
|
|
31
|
+
unprovenCount === 0,
|
|
32
|
+
provenCount,
|
|
33
|
+
unprovenCount,
|
|
34
|
+
exitCode,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// The replay eval: the lesson's hits must come from at least PROMOTION_HITS distinct
|
|
38
|
+
// sessions, and each of those sessions must still score as verified when replayed from its
|
|
39
|
+
// recorded events. A hit whose session has since vanished or no longer verifies does not count.
|
|
40
|
+
export function replayEval(store) {
|
|
41
|
+
return async (lesson) => {
|
|
42
|
+
const sessionIds = [
|
|
43
|
+
...new Set(lesson.evidence
|
|
44
|
+
.filter((ref) => ref.startsWith("session:"))
|
|
45
|
+
.map((ref) => ref.slice("session:".length))),
|
|
46
|
+
];
|
|
47
|
+
const replayed = sessionIds.filter((id) => {
|
|
48
|
+
const session = store.get(id);
|
|
49
|
+
return session
|
|
50
|
+
? scoreSession(session, store.listEvents(id)).verified
|
|
51
|
+
: false;
|
|
52
|
+
});
|
|
53
|
+
return replayed.length >= PROMOTION_HITS
|
|
54
|
+
? { passed: true, reason: `${replayed.length} sessions replay verified` }
|
|
55
|
+
: {
|
|
56
|
+
passed: false,
|
|
57
|
+
reason: `${replayed.length} of ${PROMOTION_HITS} required sessions replay verified`,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// candidate → active, only with PROMOTION_HITS verified hits and a passing eval. Never
|
|
62
|
+
// writes to charter/: that stays an owner decision.
|
|
63
|
+
export async function promoteLesson(id, evaluate) {
|
|
64
|
+
const lesson = await readLesson(id);
|
|
65
|
+
if (!lesson)
|
|
66
|
+
return { promoted: false, reason: "not found" };
|
|
67
|
+
if (lesson.status !== "candidate")
|
|
68
|
+
return { promoted: false, reason: `already ${lesson.status}` };
|
|
69
|
+
if (lesson.hits < PROMOTION_HITS)
|
|
70
|
+
return {
|
|
71
|
+
promoted: false,
|
|
72
|
+
reason: `${lesson.hits} of ${PROMOTION_HITS} verified hits`,
|
|
73
|
+
};
|
|
74
|
+
const verdict = await evaluate(lesson);
|
|
75
|
+
if (!verdict.passed)
|
|
76
|
+
return { promoted: false, reason: verdict.reason };
|
|
77
|
+
await setLessonStatus(id, "active");
|
|
78
|
+
return { promoted: true, reason: verdict.reason };
|
|
79
|
+
}
|
|
80
|
+
function lessonIdFor(signal) {
|
|
81
|
+
const digest = createHash("sha256")
|
|
82
|
+
.update(`${signal.signalType}:${signal.summary.toLocaleLowerCase()}`)
|
|
83
|
+
.digest("hex");
|
|
84
|
+
return `lesson-${digest.slice(0, 16)}`;
|
|
85
|
+
}
|
|
86
|
+
function activatedLessonIds(events) {
|
|
87
|
+
const ids = events
|
|
88
|
+
.filter((event) => event.type === "skill_auto_activated")
|
|
89
|
+
.map((event) => safeJsonParse(event.data)?.id)
|
|
90
|
+
.filter((id) => typeof id === "string");
|
|
91
|
+
return [...new Set(ids)];
|
|
92
|
+
}
|
|
93
|
+
// Called once per session at closeout. A verified session confirms the lessons it used,
|
|
94
|
+
// turns what it did into candidate lessons, and may promote a lesson that reached the bar.
|
|
95
|
+
// An explicit user correction contradicts the lessons that were active when it happened.
|
|
96
|
+
export async function learnFromSession(store, sessionId, now = new Date()) {
|
|
97
|
+
const session = store.get(sessionId);
|
|
98
|
+
if (!session)
|
|
99
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
100
|
+
const events = store.listEvents(sessionId);
|
|
101
|
+
const score = scoreSession(session, events);
|
|
102
|
+
const learning = {
|
|
103
|
+
score,
|
|
104
|
+
created: [],
|
|
105
|
+
confirmed: [],
|
|
106
|
+
contradicted: [],
|
|
107
|
+
promoted: [],
|
|
108
|
+
errors: [],
|
|
109
|
+
};
|
|
110
|
+
const evaluate = replayEval(store);
|
|
111
|
+
const corrected = hasUserCorrection(events);
|
|
112
|
+
const ref = `session:${sessionId}`;
|
|
113
|
+
// One lesson failing (an I/O or lock error) is recorded and skipped, never thrown: the
|
|
114
|
+
// changes already made to other lessons must still reach the audit event.
|
|
115
|
+
const guarded = async (id, work) => {
|
|
116
|
+
try {
|
|
117
|
+
await work();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
learning.errors.push(`${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const reinforce = (id) => guarded(id, async () => {
|
|
124
|
+
await confirmLesson(id, { evidence: ref, now });
|
|
125
|
+
learning.confirmed.push(id);
|
|
126
|
+
if ((await promoteLesson(id, evaluate)).promoted)
|
|
127
|
+
learning.promoted.push(id);
|
|
128
|
+
});
|
|
129
|
+
for (const id of activatedLessonIds(events)) {
|
|
130
|
+
await guarded(id, async () => {
|
|
131
|
+
const lesson = await readLesson(id);
|
|
132
|
+
// A session that was already counted (closeout retried) is never counted twice.
|
|
133
|
+
if (!lesson ||
|
|
134
|
+
lesson.status === "retired" ||
|
|
135
|
+
lesson.evidence.includes(ref))
|
|
136
|
+
return;
|
|
137
|
+
if (corrected) {
|
|
138
|
+
await contradictLesson(id, {
|
|
139
|
+
reason: "user corrected the session that used it",
|
|
140
|
+
evidence: ref,
|
|
141
|
+
now,
|
|
142
|
+
});
|
|
143
|
+
learning.contradicted.push(id);
|
|
144
|
+
}
|
|
145
|
+
else if (score.verified)
|
|
146
|
+
await reinforce(id);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!score.verified)
|
|
150
|
+
return learning;
|
|
151
|
+
for (const signal of detectSessionSignals(events).slice(0, MAX_NEW_LESSONS)) {
|
|
152
|
+
const id = lessonIdFor(signal);
|
|
153
|
+
await guarded(id, async () => {
|
|
154
|
+
const existing = await readLesson(id);
|
|
155
|
+
if (existing) {
|
|
156
|
+
// Seen again in another verified session: reinforcement, which counts as a hit.
|
|
157
|
+
if (existing.status !== "retired" && !existing.evidence.includes(ref))
|
|
158
|
+
await reinforce(id);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
// Evidence is the session only: event refs live in that session's own record, and
|
|
163
|
+
// a bounded list of them would push the session refs the replay eval needs out.
|
|
164
|
+
await addLesson({
|
|
165
|
+
id,
|
|
166
|
+
title: signal.summary.slice(0, 80),
|
|
167
|
+
body: signal.summary,
|
|
168
|
+
confidence: signal.confidence,
|
|
169
|
+
evidence: [ref],
|
|
170
|
+
sourceSessionId: sessionId,
|
|
171
|
+
}, now);
|
|
172
|
+
learning.created.push({
|
|
173
|
+
id,
|
|
174
|
+
signalType: signal.signalType,
|
|
175
|
+
summary: signal.summary,
|
|
176
|
+
confidence: signal.confidence,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// Refused on its own terms (e.g. the summary looks like a secret).
|
|
181
|
+
if (!(error instanceof LessonRejectedError))
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return learning;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=lesson-learning.js.map
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, rename } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { atomicWrite, parseFrontmatter, withFileLock, } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { KNOWLEDGE_DIR, oceanPath, SYSTEM_DIR, } from "@ocean/core/shared/paths.js";
|
|
6
|
+
export const LESSON_HALF_LIFE_DAYS = 30;
|
|
7
|
+
const CONFIRM_STEP = 0.1;
|
|
8
|
+
const CONTRADICT_FACTOR = 0.5;
|
|
9
|
+
const RETIRE_BELOW = 0.1;
|
|
10
|
+
const MAX_BODY_CHARS = 32_000;
|
|
11
|
+
const MAX_EVIDENCE = 20;
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,80}$/;
|
|
14
|
+
const SECRET_PATTERN = /(api[_ -]?key|token|password|secret)\s*[:=]/i;
|
|
15
|
+
export const lessonsDir = () => oceanPath(KNOWLEDGE_DIR, "lessons");
|
|
16
|
+
const lessonFile = (id) => path.join(lessonsDir(), `${id}.md`);
|
|
17
|
+
export const conflictsFile = () => path.join(lessonsDir(), "CONFLICTS.md");
|
|
18
|
+
const clamp = (value) => Math.min(1, Math.max(0, value));
|
|
19
|
+
// Confidence as of `now`: halves every LESSON_HALF_LIFE_DAYS since it was last written.
|
|
20
|
+
export function effectiveConfidence(lesson, now = new Date()) {
|
|
21
|
+
const anchor = Date.parse(lesson.confidenceAt);
|
|
22
|
+
const ageDays = Number.isNaN(anchor)
|
|
23
|
+
? 0
|
|
24
|
+
: Math.max(0, (now.getTime() - anchor) / DAY_MS);
|
|
25
|
+
return lesson.confidence * 0.5 ** (ageDays / LESSON_HALF_LIFE_DAYS);
|
|
26
|
+
}
|
|
27
|
+
function serialize(lesson) {
|
|
28
|
+
const summary = lesson.body.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
29
|
+
const fields = [
|
|
30
|
+
["id", lesson.id],
|
|
31
|
+
["title", JSON.stringify(lesson.title)],
|
|
32
|
+
["summary", JSON.stringify(summary)],
|
|
33
|
+
["type", "experiential"],
|
|
34
|
+
["status", lesson.status],
|
|
35
|
+
["confidence", lesson.confidence.toFixed(4)],
|
|
36
|
+
["confidence_at", lesson.confidenceAt],
|
|
37
|
+
["hits", String(lesson.hits)],
|
|
38
|
+
["evidence", JSON.stringify(lesson.evidence)],
|
|
39
|
+
["scope", "global"],
|
|
40
|
+
["created", lesson.created],
|
|
41
|
+
["last_confirmed_at", lesson.lastConfirmedAt ?? ""],
|
|
42
|
+
["source_session", lesson.sourceSessionId ?? ""],
|
|
43
|
+
];
|
|
44
|
+
const head = fields
|
|
45
|
+
.filter(([, value]) => value !== "")
|
|
46
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
47
|
+
.join("\n");
|
|
48
|
+
return `---\n${head}\n---\n\n${lesson.body.trim()}\n`;
|
|
49
|
+
}
|
|
50
|
+
function parse(source) {
|
|
51
|
+
const fields = parseFrontmatter(source);
|
|
52
|
+
if (fields.type !== "experiential" || !fields.id)
|
|
53
|
+
return null;
|
|
54
|
+
const status = fields.status;
|
|
55
|
+
if (!["candidate", "active", "retired"].includes(status))
|
|
56
|
+
return null;
|
|
57
|
+
let evidence = [];
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(fields.evidence ?? "[]");
|
|
60
|
+
if (Array.isArray(parsed))
|
|
61
|
+
evidence = parsed.map(String);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// A hand-edited, unparseable list is treated as no evidence rather than a lost lesson.
|
|
65
|
+
}
|
|
66
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
67
|
+
const end = normalized.indexOf("\n---", 3);
|
|
68
|
+
return {
|
|
69
|
+
id: fields.id,
|
|
70
|
+
title: unquote(fields.title ?? fields.id),
|
|
71
|
+
body: (end < 0 ? "" : normalized.slice(end + 4)).trim(),
|
|
72
|
+
status,
|
|
73
|
+
confidence: clamp(Number(fields.confidence) || 0),
|
|
74
|
+
confidenceAt: fields.confidence_at || fields.created || "",
|
|
75
|
+
hits: Math.max(0, Math.trunc(Number(fields.hits) || 0)),
|
|
76
|
+
evidence,
|
|
77
|
+
created: fields.created ?? "",
|
|
78
|
+
lastConfirmedAt: fields.last_confirmed_at || null,
|
|
79
|
+
sourceSessionId: fields.source_session || null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// parseFrontmatter strips only the outer quotes; JSON-encoded values keep inner escapes.
|
|
83
|
+
function unquote(value) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(`"${value}"`);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// A lesson the store refuses on its own terms (as opposed to an I/O failure): the migration
|
|
92
|
+
// skips these and surfaces everything else.
|
|
93
|
+
export class LessonRejectedError extends Error {
|
|
94
|
+
}
|
|
95
|
+
function assertClean(...texts) {
|
|
96
|
+
if (texts.some((text) => text !== undefined && SECRET_PATTERN.test(text)))
|
|
97
|
+
throw new LessonRejectedError("Invalid or sensitive lesson");
|
|
98
|
+
}
|
|
99
|
+
function assertValid(input) {
|
|
100
|
+
if (!ID_PATTERN.test(input.id) ||
|
|
101
|
+
!input.title.trim() ||
|
|
102
|
+
!input.body.trim() ||
|
|
103
|
+
input.body.length > MAX_BODY_CHARS)
|
|
104
|
+
throw new LessonRejectedError("Invalid or sensitive lesson");
|
|
105
|
+
assertClean(input.title, input.body, ...(input.evidence ?? []));
|
|
106
|
+
}
|
|
107
|
+
export async function readLesson(id) {
|
|
108
|
+
if (!ID_PATTERN.test(id))
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
return parse(await readFile(lessonFile(id), "utf8"));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === "ENOENT")
|
|
115
|
+
return null;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function listLessons(filter = {}) {
|
|
120
|
+
let names;
|
|
121
|
+
try {
|
|
122
|
+
names = await readdir(lessonsDir());
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code === "ENOENT")
|
|
126
|
+
return [];
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
const lessons = [];
|
|
130
|
+
for (const name of names.sort()) {
|
|
131
|
+
if (!name.endsWith(".md") || name === "CONFLICTS.md")
|
|
132
|
+
continue;
|
|
133
|
+
const lesson = await readLesson(name.slice(0, -3));
|
|
134
|
+
if (lesson && (!filter.status || lesson.status === filter.status))
|
|
135
|
+
lessons.push(lesson);
|
|
136
|
+
}
|
|
137
|
+
return lessons;
|
|
138
|
+
}
|
|
139
|
+
async function write(lesson) {
|
|
140
|
+
await mkdir(lessonsDir(), { recursive: true });
|
|
141
|
+
await atomicWrite(lessonFile(lesson.id), serialize(lesson), 0o600);
|
|
142
|
+
}
|
|
143
|
+
export async function addLesson(input, now = new Date()) {
|
|
144
|
+
assertValid(input);
|
|
145
|
+
const stamp = now.toISOString();
|
|
146
|
+
return withFileLock(lessonFile(input.id), async () => {
|
|
147
|
+
if (await readLesson(input.id))
|
|
148
|
+
throw new LessonRejectedError("Duplicate lesson");
|
|
149
|
+
const lesson = {
|
|
150
|
+
id: input.id,
|
|
151
|
+
title: input.title.trim(),
|
|
152
|
+
body: input.body.trim(),
|
|
153
|
+
status: input.status ?? "candidate",
|
|
154
|
+
confidence: clamp(input.confidence ?? 0.5),
|
|
155
|
+
confidenceAt: stamp,
|
|
156
|
+
hits: 0,
|
|
157
|
+
evidence: (input.evidence ?? []).slice(0, MAX_EVIDENCE),
|
|
158
|
+
created: stamp,
|
|
159
|
+
lastConfirmedAt: null,
|
|
160
|
+
sourceSessionId: input.sourceSessionId ?? null,
|
|
161
|
+
};
|
|
162
|
+
await write(lesson);
|
|
163
|
+
return lesson;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function update(id, change) {
|
|
167
|
+
return withFileLock(lessonFile(id), async () => {
|
|
168
|
+
const lesson = await readLesson(id);
|
|
169
|
+
if (!lesson)
|
|
170
|
+
throw new Error(`Lesson not found: ${id}`);
|
|
171
|
+
await change(lesson);
|
|
172
|
+
await write(lesson);
|
|
173
|
+
return lesson;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Confirmation on use: a verified use is a hit, restores the decayed confidence and a
|
|
177
|
+
// little more, and restarts the decay clock.
|
|
178
|
+
export async function confirmLesson(id, options = {}) {
|
|
179
|
+
const now = options.now ?? new Date();
|
|
180
|
+
assertClean(options.evidence);
|
|
181
|
+
return update(id, (lesson) => {
|
|
182
|
+
lesson.confidence = clamp(effectiveConfidence(lesson, now) + CONFIRM_STEP);
|
|
183
|
+
lesson.confidenceAt = now.toISOString();
|
|
184
|
+
lesson.lastConfirmedAt = now.toISOString();
|
|
185
|
+
lesson.hits += 1;
|
|
186
|
+
if (options.evidence && !lesson.evidence.includes(options.evidence))
|
|
187
|
+
lesson.evidence = [...lesson.evidence, options.evidence].slice(-MAX_EVIDENCE);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// The only way a lesson changes status outside contradiction. Callers own the rule that
|
|
191
|
+
// justifies the move (see lesson-learning.ts for candidate → active).
|
|
192
|
+
export async function setLessonStatus(id, status) {
|
|
193
|
+
return update(id, (lesson) => {
|
|
194
|
+
lesson.status = status;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// A contradicted fact loses confidence, the disagreement is written to CONFLICTS.md for
|
|
198
|
+
// the owner, and a lesson that falls below RETIRE_BELOW is retired (kept, never deleted).
|
|
199
|
+
export async function contradictLesson(id, options) {
|
|
200
|
+
const now = options.now ?? new Date();
|
|
201
|
+
assertClean(options.reason, options.evidence);
|
|
202
|
+
let entry = "";
|
|
203
|
+
const lesson = await update(id, (current) => {
|
|
204
|
+
const before = effectiveConfidence(current, now);
|
|
205
|
+
current.confidence = clamp(before * CONTRADICT_FACTOR);
|
|
206
|
+
current.confidenceAt = now.toISOString();
|
|
207
|
+
if (options.evidence && !current.evidence.includes(options.evidence))
|
|
208
|
+
current.evidence = [...current.evidence, options.evidence].slice(-MAX_EVIDENCE);
|
|
209
|
+
if (current.confidence < RETIRE_BELOW)
|
|
210
|
+
current.status = "retired";
|
|
211
|
+
const reason = options.reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
212
|
+
entry =
|
|
213
|
+
`- ${now.toISOString()} \`${current.id}\` contradicted: ${reason}` +
|
|
214
|
+
` (confidence ${before.toFixed(2)} → ${current.confidence.toFixed(2)}` +
|
|
215
|
+
`${current.status === "retired" ? ", retired" : ""}` +
|
|
216
|
+
`${options.evidence ? `; evidence ${options.evidence}` : ""})\n`;
|
|
217
|
+
});
|
|
218
|
+
// After the lesson is written, so the log never records a change that did not happen.
|
|
219
|
+
await appendConflict(entry);
|
|
220
|
+
return lesson;
|
|
221
|
+
}
|
|
222
|
+
async function appendConflict(line) {
|
|
223
|
+
await mkdir(lessonsDir(), { recursive: true });
|
|
224
|
+
await withFileLock(conflictsFile(), async () => {
|
|
225
|
+
const current = await readFile(conflictsFile(), "utf8").catch((error) => {
|
|
226
|
+
if (error.code === "ENOENT")
|
|
227
|
+
return "# Lesson conflicts\n\nContradicted lessons, newest last. Resolve by hand.\n\n";
|
|
228
|
+
throw error;
|
|
229
|
+
});
|
|
230
|
+
await atomicWrite(conflictsFile(), `${current}${line}`, 0o600);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async function readLegacy(file) {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
236
|
+
if (!Array.isArray(parsed))
|
|
237
|
+
throw new Error(`Legacy lesson store is not a JSON array: ${file}`);
|
|
238
|
+
return parsed;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error.code === "ENOENT")
|
|
242
|
+
return null;
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// `lesson-<id>`, lowercased to the id alphabet. A raw id that does not survive that unchanged
|
|
247
|
+
// (or is too long) gets a hash suffix so two distinct legacy ids can never collide.
|
|
248
|
+
function legacyLessonId(raw) {
|
|
249
|
+
const slug = `lesson-${raw}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
250
|
+
if (slug === `lesson-${raw}` && slug.length <= 70)
|
|
251
|
+
return slug;
|
|
252
|
+
return `${slug.slice(0, 60)}-${createHash("sha256").update(raw).digest("hex").slice(0, 8)}`;
|
|
253
|
+
}
|
|
254
|
+
// Folds the two legacy lesson stores (skills/observations.json and skills/candidates.json)
|
|
255
|
+
// into lessons, then renames each to `*.migrated` — never deleted. Idempotent: an existing
|
|
256
|
+
// lesson id is skipped, and a renamed file is not read again. `system/observations/*.json`
|
|
257
|
+
// is not migrated: those are raw `ocean observe` command logs, evidence a lesson can cite.
|
|
258
|
+
export async function migrateLegacyLessons(now = new Date()) {
|
|
259
|
+
const skillsDir = oceanPath(SYSTEM_DIR, "skills");
|
|
260
|
+
const observationsFile = path.join(skillsDir, "observations.json");
|
|
261
|
+
const candidatesFile = path.join(skillsDir, "candidates.json");
|
|
262
|
+
const result = { migrated: 0, skipped: 0 };
|
|
263
|
+
const put = async (input, retired) => {
|
|
264
|
+
try {
|
|
265
|
+
await addLesson({ ...input, status: retired ? "retired" : input.status }, now);
|
|
266
|
+
result.migrated += 1;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (!(error instanceof LessonRejectedError))
|
|
270
|
+
throw error;
|
|
271
|
+
result.skipped += 1;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
// Both files are read before anything is written: a corrupt one aborts a clean no-op.
|
|
275
|
+
const candidates = await readLegacy(candidatesFile);
|
|
276
|
+
const observations = await readLegacy(observationsFile);
|
|
277
|
+
const covered = new Set();
|
|
278
|
+
for (const item of candidates ?? []) {
|
|
279
|
+
if (!item.id || !item.instructions) {
|
|
280
|
+
result.skipped += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
covered.add(item.id);
|
|
284
|
+
const promoted = item.status === "promoted" && item.verification === "owner-reviewed";
|
|
285
|
+
await put({
|
|
286
|
+
id: legacyLessonId(item.id),
|
|
287
|
+
title: item.name ?? item.id,
|
|
288
|
+
body: item.instructions,
|
|
289
|
+
status: promoted ? "active" : "candidate",
|
|
290
|
+
confidence: promoted ? 0.8 : 0.5,
|
|
291
|
+
sourceSessionId: item.sourceSessionId,
|
|
292
|
+
}, item.status === "rejected");
|
|
293
|
+
}
|
|
294
|
+
for (const item of observations ?? []) {
|
|
295
|
+
if (!item.observationId || !item.summary) {
|
|
296
|
+
result.skipped += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// An approved observation already became a candidate; that candidate is the lesson.
|
|
300
|
+
if (item.skillCandidateId && covered.has(item.skillCandidateId)) {
|
|
301
|
+
result.skipped += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
await put({
|
|
305
|
+
id: legacyLessonId(item.observationId),
|
|
306
|
+
title: item.summary.slice(0, 80),
|
|
307
|
+
body: item.summary,
|
|
308
|
+
confidence: item.confidence,
|
|
309
|
+
evidence: item.evidenceRefs,
|
|
310
|
+
sourceSessionId: item.sourceSessionId,
|
|
311
|
+
}, item.status === "discarded" || item.status === "rejected");
|
|
312
|
+
}
|
|
313
|
+
for (const [file, existed] of [
|
|
314
|
+
[candidatesFile, candidates],
|
|
315
|
+
[observationsFile, observations],
|
|
316
|
+
])
|
|
317
|
+
if (existed)
|
|
318
|
+
await rename(file, `${file}.migrated`);
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=lessons.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { appendFile, mkdir, open, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { withFileLock } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { auditFile } from "./memory-paths.js";
|
|
7
|
+
export const DEFAULT_RETENTION_DAYS = 90;
|
|
8
|
+
export function sha256(content) {
|
|
9
|
+
return createHash("sha256").update(content).digest("hex");
|
|
10
|
+
}
|
|
11
|
+
// Sortable by time, unique enough for one workspace: 20261030161550-1a2b3c4d.
|
|
12
|
+
export function newId(now = new Date()) {
|
|
13
|
+
const stamp = now
|
|
14
|
+
.toISOString()
|
|
15
|
+
.replace(/[-:TZ]/g, "")
|
|
16
|
+
.slice(0, 14);
|
|
17
|
+
return `${stamp}-${randomBytes(4).toString("hex")}`;
|
|
18
|
+
}
|
|
19
|
+
export function idTime(id) {
|
|
20
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})-/.exec(id);
|
|
21
|
+
if (!match)
|
|
22
|
+
return null;
|
|
23
|
+
const [, y, mo, d, h, mi, s] = match;
|
|
24
|
+
return new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}Z`);
|
|
25
|
+
}
|
|
26
|
+
// A stored copy is written once and fsynced before the record it preserves is replaced.
|
|
27
|
+
export async function writeDurable(file, content) {
|
|
28
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
29
|
+
const handle = await open(file, "wx", 0o600);
|
|
30
|
+
try {
|
|
31
|
+
await handle.writeFile(content, "utf8");
|
|
32
|
+
await handle.sync();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function appendAudit(entry) {
|
|
39
|
+
const full = {
|
|
40
|
+
...entry,
|
|
41
|
+
id: entry.id ?? newId(),
|
|
42
|
+
ts: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
const file = auditFile();
|
|
45
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
46
|
+
await withFileLock(file, () => appendFile(file, `${JSON.stringify(full)}\n`, { mode: 0o600 }));
|
|
47
|
+
return full;
|
|
48
|
+
}
|
|
49
|
+
export async function readAudit() {
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = await readFile(auditFile(), "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return [];
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return text
|
|
60
|
+
.split("\n")
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.map((line) => JSON.parse(line));
|
|
63
|
+
}
|
|
64
|
+
// bridge/config/memory.json — optional; without it MCP writes are on, copies are kept 90 days and
|
|
65
|
+
// pulled memory waits for review.
|
|
66
|
+
export async function memorySettings() {
|
|
67
|
+
const file = oceanPath(SYSTEM_DIR, "config", "memory.json");
|
|
68
|
+
let input;
|
|
69
|
+
try {
|
|
70
|
+
input = JSON.parse(await readFile(file, "utf8"));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code === "ENOENT")
|
|
74
|
+
return {
|
|
75
|
+
mcpWrites: true,
|
|
76
|
+
retentionDays: DEFAULT_RETENTION_DAYS,
|
|
77
|
+
pullAutoApprove: false,
|
|
78
|
+
proposeAutoApprove: false,
|
|
79
|
+
};
|
|
80
|
+
throw new Error(`Invalid memory settings in ${file}`);
|
|
81
|
+
}
|
|
82
|
+
const writes = input.mcpWrites ?? "on";
|
|
83
|
+
const days = input.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
84
|
+
const pullApprove = input.pullAutoApprove ?? "off";
|
|
85
|
+
const proposeApprove = input.proposeAutoApprove ?? "off";
|
|
86
|
+
if ((writes !== "on" && writes !== "off") ||
|
|
87
|
+
(pullApprove !== "on" && pullApprove !== "off") ||
|
|
88
|
+
(proposeApprove !== "project" && proposeApprove !== "off") ||
|
|
89
|
+
typeof days !== "number" ||
|
|
90
|
+
!Number.isInteger(days) ||
|
|
91
|
+
days < 1)
|
|
92
|
+
throw new Error(`Invalid memory settings in ${file}`);
|
|
93
|
+
return {
|
|
94
|
+
mcpWrites: writes === "on",
|
|
95
|
+
retentionDays: days,
|
|
96
|
+
pullAutoApprove: pullApprove === "on",
|
|
97
|
+
proposeAutoApprove: proposeApprove === "project",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=memory-audit.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
3
|
+
import { listProjectDirs, oceanPath, PERSONAL_DIR, } from "@ocean/core/shared/paths.js";
|
|
4
|
+
import { readAudit } from "./memory-audit.js";
|
|
5
|
+
import { listCandidates } from "./memory-review.js";
|
|
6
|
+
// The memory reminder for /day-start (T-296 §3.10): at most five lines, each only when there is
|
|
7
|
+
// something to act on. It reads; it never edits a record.
|
|
8
|
+
const DAY = 86_400_000;
|
|
9
|
+
async function openConflicts() {
|
|
10
|
+
const text = await readFile(oceanPath(PERSONAL_DIR, "CONFLICTS.md"), "utf8").catch(() => "");
|
|
11
|
+
// A conflict is a `## ` section; the template's `## <date> — <topic>` is not one.
|
|
12
|
+
return text.split("\n").filter((line) => /^## (?!<)/.test(line)).length;
|
|
13
|
+
}
|
|
14
|
+
async function projectsWithoutMemory() {
|
|
15
|
+
const missing = [];
|
|
16
|
+
for (const { id, dir } of listProjectDirs()) {
|
|
17
|
+
const isProject = await stat(oceanPath(dir, "project.json"))
|
|
18
|
+
.then(() => true)
|
|
19
|
+
.catch(() => false);
|
|
20
|
+
if (!isProject)
|
|
21
|
+
continue;
|
|
22
|
+
const records = await readdir(oceanPath(dir, "memory")).catch(() => []);
|
|
23
|
+
if (!records.some((name) => name.endsWith(".md") && name !== "MEMORY.md"))
|
|
24
|
+
missing.push(id);
|
|
25
|
+
}
|
|
26
|
+
return missing.sort();
|
|
27
|
+
}
|
|
28
|
+
export async function memoryDigest(options) {
|
|
29
|
+
const now = options.now ?? new Date();
|
|
30
|
+
const lines = [];
|
|
31
|
+
const conflicts = await openConflicts();
|
|
32
|
+
if (conflicts)
|
|
33
|
+
lines.push(`memory: ${conflicts} conflict(s) wait in 02-personal/CONFLICTS.md`);
|
|
34
|
+
const old = (await listCandidates()).filter((item) => now.getTime() - Date.parse(item.created) > 3 * DAY);
|
|
35
|
+
if (old.length)
|
|
36
|
+
lines.push(`memory: ${old.length} candidate(s) older than 3 days — ocean memory review`);
|
|
37
|
+
if (options.stale)
|
|
38
|
+
lines.push("memory: search index is stale — ocean memory reindex");
|
|
39
|
+
const bare = await projectsWithoutMemory();
|
|
40
|
+
if (bare.length)
|
|
41
|
+
lines.push(`memory: no records yet for ${bare.slice(0, 5).join(", ")}${bare.length > 5 ? ` (+${bare.length - 5})` : ""}`);
|
|
42
|
+
const yesterday = localDate(new Date(now.getTime() - DAY));
|
|
43
|
+
const writes = (await readAudit()).filter((entry) => (entry.action === "create" || entry.action === "update") &&
|
|
44
|
+
localDate(new Date(entry.ts)) === yesterday);
|
|
45
|
+
if (writes.length)
|
|
46
|
+
lines.push(`memory: ${writes.length} direct write(s) yesterday — ocean memory history`);
|
|
47
|
+
return lines;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=memory-digest.js.map
|