@amoree-code/ocean 0.0.0-stage → 1.1.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.
Files changed (304) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -2
  3. package/dist/application/auth/auth-orchestrator.js +77 -0
  4. package/dist/application/clients/clients.js +646 -0
  5. package/dist/application/clients/editors.js +398 -0
  6. package/dist/application/clients/hooks.js +335 -0
  7. package/dist/application/clients/native.js +410 -0
  8. package/dist/application/clients/opencode-plugin.js +158 -0
  9. package/dist/application/clients/registry.js +188 -0
  10. package/dist/application/gateway/webhook-gateway.js +137 -0
  11. package/dist/application/install/provider-installer.js +184 -0
  12. package/dist/application/integrations/charter-copies.js +76 -0
  13. package/dist/application/integrations/claude-vscode.js +106 -0
  14. package/dist/application/integrations/engine-entry-repair.js +105 -0
  15. package/dist/application/integrations/skill-filter.js +254 -0
  16. package/dist/application/layout/layout-apply.js +443 -0
  17. package/dist/application/layout/layout-plan.js +236 -0
  18. package/dist/application/layout/workspace-template.js +34 -0
  19. package/dist/application/onboarding/state.js +23 -0
  20. package/dist/application/onboarding/steps.js +249 -0
  21. package/dist/application/profiles/profile-distribution.js +53 -0
  22. package/dist/application/profiles/profile-suggestion.js +39 -0
  23. package/dist/application/profiles/profile-upgrade-service.js +208 -0
  24. package/dist/application/pull/history-index.js +476 -0
  25. package/dist/application/pull/project-match.js +132 -0
  26. package/dist/application/pull/pull.js +202 -0
  27. package/dist/application/pull/server-capture.js +307 -0
  28. package/dist/application/scheduler/local-scheduler.js +210 -0
  29. package/dist/composition/runtime.js +64 -0
  30. package/dist/engine-root.js +9 -0
  31. package/dist/infrastructure/filesystem/profile-loader.js +46 -0
  32. package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
  33. package/dist/infrastructure/filesystem/skill-loader.js +84 -0
  34. package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
  35. package/dist/infrastructure/observability/runtime-logger.js +16 -0
  36. package/dist/infrastructure/process/agent-tool-executor.js +99 -0
  37. package/dist/infrastructure/process/cli-process.js +49 -0
  38. package/dist/infrastructure/process/interactive-process.js +145 -0
  39. package/dist/infrastructure/process/process-supervisor.js +194 -0
  40. package/dist/infrastructure/process/service.js +252 -0
  41. package/dist/infrastructure/providers/browser-provider.js +2 -0
  42. package/dist/infrastructure/providers/claude-sdk.js +137 -0
  43. package/dist/infrastructure/providers/client-home.js +14 -0
  44. package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
  45. package/dist/infrastructure/providers/ollama-embedder.js +74 -0
  46. package/dist/infrastructure/providers/openai-compatible.js +268 -0
  47. package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
  48. package/dist/infrastructure/providers/provider-capabilities.js +50 -0
  49. package/dist/infrastructure/providers/provider-invocation.js +184 -0
  50. package/dist/infrastructure/providers/provider-registry.js +77 -0
  51. package/dist/infrastructure/providers/providers.js +134 -0
  52. package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
  53. package/dist/interfaces/cli/auth-command.js +54 -0
  54. package/dist/interfaces/cli/browser-bridge-command.js +93 -0
  55. package/dist/interfaces/cli/browser-command.js +119 -0
  56. package/dist/interfaces/cli/capture-command.js +125 -0
  57. package/dist/interfaces/cli/client-test-command.js +133 -0
  58. package/dist/interfaces/cli/clients-command.js +129 -0
  59. package/dist/interfaces/cli/command-registry.js +1211 -0
  60. package/dist/interfaces/cli/context-command.js +172 -0
  61. package/dist/interfaces/cli/daily-command.js +131 -0
  62. package/dist/interfaces/cli/governance-command.js +55 -0
  63. package/dist/interfaces/cli/handoff-command.js +35 -0
  64. package/dist/interfaces/cli/help-command.js +207 -0
  65. package/dist/interfaces/cli/idea-command.js +47 -0
  66. package/dist/interfaces/cli/intercept-command.js +317 -0
  67. package/dist/interfaces/cli/layout-command.js +27 -0
  68. package/dist/interfaces/cli/market-command.js +52 -0
  69. package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
  70. package/dist/interfaces/cli/memory-command.js +317 -0
  71. package/dist/interfaces/cli/migrate-command.js +26 -0
  72. package/dist/interfaces/cli/observe-command.js +78 -0
  73. package/dist/interfaces/cli/onboard-command.js +123 -0
  74. package/dist/interfaces/cli/operate-command.js +59 -0
  75. package/dist/interfaces/cli/profiles-command.js +70 -0
  76. package/dist/interfaces/cli/pull-command.js +54 -0
  77. package/dist/interfaces/cli/setup-command.js +143 -0
  78. package/dist/interfaces/cli/tasks-command.js +249 -0
  79. package/dist/main.js +49 -0
  80. package/dist/node-warnings.js +17 -0
  81. package/dist/version.js +8 -0
  82. package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
  83. package/market/mcp/gateway/ocean.capability.json +21 -0
  84. package/market/mcp/obsidian/ocean.capability.json +21 -0
  85. package/market/mcp/ocean/ocean.capability.json +21 -0
  86. package/market/mcp/playwright/ocean.capability.json +23 -0
  87. package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
  88. package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
  89. package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
  90. package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
  91. package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
  92. package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
  93. package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
  94. package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
  95. package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
  96. package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
  97. package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
  98. package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
  99. package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
  100. package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
  101. package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
  102. package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
  103. package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
  104. package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
  105. package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
  106. package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
  107. package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
  108. package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
  109. package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
  110. package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
  111. package/node_modules/@ocean/brain/dist/memory/signal-detection.js +66 -0
  112. package/node_modules/@ocean/brain/package.json +11 -0
  113. package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
  114. package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
  115. package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
  116. package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
  117. package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
  118. package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
  119. package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
  120. package/node_modules/@ocean/context/package.json +11 -0
  121. package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
  122. package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
  123. package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
  124. package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
  125. package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
  126. package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
  127. package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
  128. package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
  129. package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
  130. package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
  131. package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
  132. package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
  133. package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
  134. package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
  135. package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
  136. package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
  137. package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
  138. package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
  139. package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
  140. package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
  141. package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
  142. package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
  143. package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
  144. package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
  145. package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
  146. package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
  147. package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
  148. package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
  149. package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
  150. package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
  151. package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
  152. package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
  153. package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
  154. package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
  155. package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
  156. package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
  157. package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
  158. package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
  159. package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
  160. package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
  161. package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
  162. package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
  163. package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
  164. package/node_modules/@ocean/core/package.json +14 -0
  165. package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
  166. package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
  167. package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
  168. package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
  169. package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
  170. package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
  171. package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
  172. package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
  173. package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
  174. package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
  175. package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
  176. package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
  177. package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
  178. package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
  179. package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
  180. package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
  181. package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
  182. package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
  183. package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
  184. package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
  185. package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
  186. package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
  187. package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
  188. package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
  189. package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
  190. package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
  191. package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
  192. package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
  193. package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
  194. package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
  195. package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
  196. package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
  197. package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
  198. package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
  199. package/node_modules/@ocean/loop/package.json +11 -0
  200. package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
  201. package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
  202. package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
  203. package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
  204. package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
  205. package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
  206. package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
  207. package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
  208. package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
  209. package/node_modules/@ocean/market/dist/install/lock.js +34 -0
  210. package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
  211. package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
  212. package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
  213. package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
  214. package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
  215. package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
  216. package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
  217. package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
  218. package/node_modules/@ocean/market/dist/security/sandbox.js +74 -0
  219. package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
  220. package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
  221. package/node_modules/@ocean/market/dist/skills/skill-curation.js +123 -0
  222. package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
  223. package/node_modules/@ocean/market/dist/skills/task-observer.js +165 -0
  224. package/node_modules/@ocean/market/package.json +11 -0
  225. package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
  226. package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
  227. package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
  228. package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
  229. package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
  230. package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
  231. package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
  232. package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
  233. package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
  234. package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
  235. package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
  236. package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
  237. package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
  238. package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
  239. package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
  240. package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
  241. package/node_modules/@ocean/services/package.json +11 -0
  242. package/ocean-marketplace.json +415 -0
  243. package/package.json +67 -4
  244. package/skills/core/business-logic/SKILL.md +22 -0
  245. package/skills/core/catch-up/SKILL.md +17 -0
  246. package/skills/core/core-thinking/SKILL.md +11 -0
  247. package/skills/core/design-thinking/SKILL.md +19 -0
  248. package/skills/core/graft/SKILL.md +156 -0
  249. package/skills/core/loop/SKILL.md +20 -0
  250. package/skills/core/session-handoff/SKILL.md +15 -0
  251. package/skills/core/use-browser/SKILL.md +69 -0
  252. package/skills/core/verification/SKILL.md +11 -0
  253. package/skills/index.json +56 -0
  254. package/templates/README.md +4 -0
  255. package/templates/profiles/default.json +51 -0
  256. package/templates/profiles/developer.json +51 -0
  257. package/templates/profiles/reviewer.json +51 -0
  258. package/templates/profiles/strategist.json +51 -0
  259. package/templates/workspace/00-inbox/INBOX.md +7 -0
  260. package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
  261. package/templates/workspace/00-inbox/native/.gitkeep +0 -0
  262. package/templates/workspace/01-daily/.gitkeep +0 -0
  263. package/templates/workspace/02-personal/CONFLICTS.md +7 -0
  264. package/templates/workspace/02-personal/MEMORY.md +15 -0
  265. package/templates/workspace/02-personal/goals/.gitkeep +0 -0
  266. package/templates/workspace/02-personal/identity/.gitkeep +0 -0
  267. package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
  268. package/templates/workspace/03-professional/.gitkeep +0 -0
  269. package/templates/workspace/04-projects/PROJECTS.md +5 -0
  270. package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
  271. package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
  272. package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
  273. package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
  274. package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
  275. package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
  276. package/templates/workspace/06-templates/TEMPLATES.md +25 -0
  277. package/templates/workspace/06-templates/decision.md +8 -0
  278. package/templates/workspace/06-templates/intelligence.md +32 -0
  279. package/templates/workspace/06-templates/project-claude.md +48 -0
  280. package/templates/workspace/06-templates/session.md +38 -0
  281. package/templates/workspace/06-templates/task.md +42 -0
  282. package/templates/workspace/MANIFEST.json +51 -0
  283. package/templates/workspace/bridge/config/CONFIG.md +21 -0
  284. package/templates/workspace/bridge/config/runtime.json +4 -0
  285. package/templates/workspace/charter/core.md +104 -0
  286. package/templates/workspace/charter/policies/README.md +31 -0
  287. package/templates/workspace/charter/policies/claude-code-context.md +90 -0
  288. package/templates/workspace/charter/policies/concise-output.md +29 -0
  289. package/templates/workspace/charter/policies/context.md +91 -0
  290. package/templates/workspace/charter/policies/contracts/task.md +189 -0
  291. package/templates/workspace/charter/policies/git.md +60 -0
  292. package/templates/workspace/charter/policies/graph.md +47 -0
  293. package/templates/workspace/charter/policies/knowledge.md +33 -0
  294. package/templates/workspace/charter/policies/lifecycle.md +119 -0
  295. package/templates/workspace/charter/policies/memory.md +67 -0
  296. package/templates/workspace/charter/policies/minimal-change.md +43 -0
  297. package/templates/workspace/charter/policies/models.md +57 -0
  298. package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
  299. package/templates/workspace/charter/policies/response.md +39 -0
  300. package/templates/workspace/charter/policies/skill-observer.md +44 -0
  301. package/templates/workspace/charter/policies/stack.md +23 -0
  302. package/templates/workspace/charter/policies/task.md +139 -0
  303. package/templates/workspace/charter/policies/verification.md +29 -0
  304. 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