@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,150 @@
|
|
|
1
|
+
import { writeBrainDump } from "@ocean/brain/memory/brain-dump.js";
|
|
2
|
+
import { appendDailyNarrative, appendObservations, } from "@ocean/brain/memory/daily-narrative.js";
|
|
3
|
+
import { learnFromSession, } from "@ocean/brain/memory/lesson-learning.js";
|
|
4
|
+
import { generateModelNarrative } from "@ocean/brain/memory/model-narrative.js";
|
|
5
|
+
import { writeSessionSummary, } from "@ocean/brain/memory/session-summary.js";
|
|
6
|
+
import { safeJsonParse } from "@ocean/core/shared/fs-utils.js";
|
|
7
|
+
import { gitChangedFiles } from "@ocean/core/shared/project-resolution.js";
|
|
8
|
+
import { createHandoffWithStore } from "../handoff/handoff-service.js";
|
|
9
|
+
async function changedFiles(workingDirectory) {
|
|
10
|
+
return (await gitChangedFiles(workingDirectory)).slice(0, 100);
|
|
11
|
+
}
|
|
12
|
+
function lastEvent(events, type) {
|
|
13
|
+
return [...events].reverse().find((event) => event.type === type);
|
|
14
|
+
}
|
|
15
|
+
function evidence(events, result) {
|
|
16
|
+
return events
|
|
17
|
+
.filter((event) => event.type === "evidence" || event.type === "provider_blocked")
|
|
18
|
+
.map((event) => {
|
|
19
|
+
const value = safeJsonParse(event.data);
|
|
20
|
+
if (event.type === "provider_blocked")
|
|
21
|
+
return result === "blocked"
|
|
22
|
+
? String(value?.reason ?? event.data)
|
|
23
|
+
: null;
|
|
24
|
+
const proven = value?.result === "proven";
|
|
25
|
+
if (result === "proven" && proven)
|
|
26
|
+
return String(value?.criterion ?? "verified result");
|
|
27
|
+
if (result === "notProven" && !proven)
|
|
28
|
+
return String(value?.criterion ?? value?.result ?? "not proven");
|
|
29
|
+
return null;
|
|
30
|
+
})
|
|
31
|
+
.filter((value) => Boolean(value))
|
|
32
|
+
.slice(0, 12);
|
|
33
|
+
}
|
|
34
|
+
export async function finalizeSession(store, sessionId, input = {}) {
|
|
35
|
+
const session = store.get(sessionId);
|
|
36
|
+
if (!session)
|
|
37
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
38
|
+
// finalizeSession can legitimately be invoked more than once for the same session
|
|
39
|
+
// (e.g. a caller's success path completes closeout, then a later step in that same
|
|
40
|
+
// caller throws and its catch block calls finalizeSession again). The summary/handoff
|
|
41
|
+
// work below is already idempotent (guarded by existing handoffId / session_summary
|
|
42
|
+
// event checks), but the daily-narrative append is a pure side effect with no such
|
|
43
|
+
// check, so a re-run would otherwise write a duplicate "Work log" line. closedAt is
|
|
44
|
+
// only ever set by the updateCloseout call further down, so its presence here means
|
|
45
|
+
// this session has already been closed out once.
|
|
46
|
+
const alreadyClosedOut = Boolean(session.closedAt);
|
|
47
|
+
const events = store.listEvents(sessionId);
|
|
48
|
+
const exitEvent = lastEvent(events, "process_exit");
|
|
49
|
+
const exitCode = input.exitCode ??
|
|
50
|
+
(exitEvent
|
|
51
|
+
? Number(safeJsonParse(exitEvent.data)?.exitCode ?? 1)
|
|
52
|
+
: session.status === "completed"
|
|
53
|
+
? 0
|
|
54
|
+
: 1);
|
|
55
|
+
const closeoutStatus = session.status === "completed" && exitCode === 0 ? "completed" : "failed";
|
|
56
|
+
const files = await changedFiles(session.workingDirectory);
|
|
57
|
+
const summary = await writeSessionSummary({
|
|
58
|
+
session,
|
|
59
|
+
events,
|
|
60
|
+
changedFiles: files,
|
|
61
|
+
exitCode,
|
|
62
|
+
nextAction: input.nextAction,
|
|
63
|
+
});
|
|
64
|
+
let handoffId = session.handoffId;
|
|
65
|
+
const enoughEvidence = Boolean(session.taskId ||
|
|
66
|
+
(closeoutStatus === "completed" &&
|
|
67
|
+
events.some((event) => ["user_input", "provider_output", "text", "json"].includes(event.type))));
|
|
68
|
+
if (!handoffId && enoughEvidence) {
|
|
69
|
+
try {
|
|
70
|
+
const handoff = await createHandoffWithStore(store, {
|
|
71
|
+
sessionId,
|
|
72
|
+
taskId: session.taskId ?? undefined,
|
|
73
|
+
nextAction: input.nextAction,
|
|
74
|
+
sourceSummaryPath: summary.summaryPath,
|
|
75
|
+
changedFiles: files,
|
|
76
|
+
verification: evidence(events, "proven"),
|
|
77
|
+
notProven: evidence(events, "notProven"),
|
|
78
|
+
blocked: evidence(events, "blocked"),
|
|
79
|
+
});
|
|
80
|
+
handoffId = handoff.handoffId;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
store.appendEvent(sessionId, "closeout_warning", error instanceof Error ? error.message : String(error));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const closedAt = new Date().toISOString();
|
|
87
|
+
store.updateCloseout(sessionId, { ...summary, closeoutStatus, closedAt });
|
|
88
|
+
if (!alreadyClosedOut) {
|
|
89
|
+
// Computed first so writeBrainDump can surface the same signals as the
|
|
90
|
+
// daily narrative, but kept in its own try/catch: a failure here must
|
|
91
|
+
// not skip the narrative/summary work below, and vice versa.
|
|
92
|
+
let observations = [];
|
|
93
|
+
try {
|
|
94
|
+
const learning = await learnFromSession(store, sessionId);
|
|
95
|
+
observations = learning.created;
|
|
96
|
+
// Only when the session changed the store: a no-op closeout adds no event.
|
|
97
|
+
if (learning.created.length ||
|
|
98
|
+
learning.confirmed.length ||
|
|
99
|
+
learning.contradicted.length ||
|
|
100
|
+
learning.promoted.length ||
|
|
101
|
+
learning.errors.length)
|
|
102
|
+
store.appendEvent(sessionId, "lesson_scored", JSON.stringify({
|
|
103
|
+
verified: learning.score.verified,
|
|
104
|
+
created: learning.created.map((lesson) => lesson.id),
|
|
105
|
+
confirmed: learning.confirmed,
|
|
106
|
+
contradicted: learning.contradicted,
|
|
107
|
+
promoted: learning.promoted,
|
|
108
|
+
errors: learning.errors,
|
|
109
|
+
}));
|
|
110
|
+
await appendObservations(session, observations);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
store.appendEvent(sessionId, "closeout_warning", error instanceof Error ? error.message : String(error));
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const narrative = await generateModelNarrative({
|
|
117
|
+
session,
|
|
118
|
+
events,
|
|
119
|
+
changedFiles: files,
|
|
120
|
+
nextAction: input.nextAction,
|
|
121
|
+
});
|
|
122
|
+
await appendDailyNarrative({
|
|
123
|
+
session,
|
|
124
|
+
events,
|
|
125
|
+
exitCode,
|
|
126
|
+
nextAction: input.nextAction,
|
|
127
|
+
narrative,
|
|
128
|
+
});
|
|
129
|
+
await writeBrainDump({
|
|
130
|
+
session,
|
|
131
|
+
events,
|
|
132
|
+
changedFiles: files,
|
|
133
|
+
exitCode,
|
|
134
|
+
nextAction: input.nextAction,
|
|
135
|
+
narrative,
|
|
136
|
+
observations,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
store.appendEvent(sessionId, "closeout_warning", error instanceof Error ? error.message : String(error));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!store
|
|
144
|
+
.listEvents(sessionId)
|
|
145
|
+
.some((event) => event.type === "session_summary")) {
|
|
146
|
+
store.appendEvent(sessionId, "session_summary", JSON.stringify({ ...summary, handoffId, closeoutStatus, closedAt }));
|
|
147
|
+
}
|
|
148
|
+
return { ...summary, handoffId, closeoutStatus };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=session-closeout.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { validateBudget } from "@ocean/context/context/context-ladder.js";
|
|
3
|
+
import { sameProject } from "@ocean/core/shared/paths.js";
|
|
4
|
+
import { resolveProject } from "@ocean/core/shared/project-resolution.js";
|
|
5
|
+
// Sessions are created with randomUUID(), so a pointer that is not a UUID did not come from
|
|
6
|
+
// Ocean and is refused before any lookup.
|
|
7
|
+
const SESSION_ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
const STALE_RESUME_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const MAX_NEXT_ACTION_CHARS = 200;
|
|
10
|
+
export function validateSessionIdentifier(sessionId) {
|
|
11
|
+
if (typeof sessionId !== "string" || sessionId.length === 0)
|
|
12
|
+
return { valid: false, reason: "session pointer is missing" };
|
|
13
|
+
if (sessionId.includes("\0"))
|
|
14
|
+
return { valid: false, reason: "session pointer contains a null byte" };
|
|
15
|
+
if (!SESSION_ID_SHAPE.test(sessionId))
|
|
16
|
+
return {
|
|
17
|
+
valid: false,
|
|
18
|
+
reason: `session pointer '${sessionId}' is not a valid Ocean session identifier`,
|
|
19
|
+
};
|
|
20
|
+
return { valid: true, value: sessionId };
|
|
21
|
+
}
|
|
22
|
+
function refused(reason, violations = []) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
mode: "refused",
|
|
26
|
+
reason,
|
|
27
|
+
pointer: null,
|
|
28
|
+
parentSessionId: null,
|
|
29
|
+
violations,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function clip(value) {
|
|
33
|
+
return value.length > MAX_NEXT_ACTION_CHARS
|
|
34
|
+
? `${value.slice(0, MAX_NEXT_ACTION_CHARS)}…`
|
|
35
|
+
: value;
|
|
36
|
+
}
|
|
37
|
+
async function projectIdFor(directory) {
|
|
38
|
+
const resolution = await resolveProject(directory);
|
|
39
|
+
return resolution.status === "bound" ? resolution.projectId : null;
|
|
40
|
+
}
|
|
41
|
+
// Builds the compact pointer for a session that is already known to exist. Carries only
|
|
42
|
+
// references: summaryPath is a path, not the summary; nextAction is clipped.
|
|
43
|
+
export async function buildSessionPointer(session) {
|
|
44
|
+
return {
|
|
45
|
+
sessionId: session.sessionId,
|
|
46
|
+
parentSessionId: session.parentSessionId,
|
|
47
|
+
projectId: await projectIdFor(session.workingDirectory),
|
|
48
|
+
provider: session.provider,
|
|
49
|
+
status: session.status,
|
|
50
|
+
taskId: session.taskId,
|
|
51
|
+
checkpointRef: session.summaryPath,
|
|
52
|
+
nextAction: clip(session.nextAction ?? ""),
|
|
53
|
+
updatedAt: session.updatedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Deterministic, read-only resume planning. Budget is validated before the store is read.
|
|
57
|
+
export async function planSessionResume(store, sessionId, budget, options = {}) {
|
|
58
|
+
const budgetCheck = validateBudget(budget);
|
|
59
|
+
if (!budgetCheck.valid)
|
|
60
|
+
return refused(`invalid-budget: ${budgetCheck.reason}`, ["invalid-budget"]);
|
|
61
|
+
const identifier = validateSessionIdentifier(sessionId);
|
|
62
|
+
if (!identifier.valid)
|
|
63
|
+
return refused(identifier.reason);
|
|
64
|
+
const session = store.get(identifier.value);
|
|
65
|
+
if (!session)
|
|
66
|
+
return refused(`session ${identifier.value} does not exist in Ocean`);
|
|
67
|
+
const pointer = await buildSessionPointer(session);
|
|
68
|
+
const cwd = options.cwd;
|
|
69
|
+
if (cwd) {
|
|
70
|
+
const currentProject = await projectIdFor(path.resolve(cwd));
|
|
71
|
+
if (pointer.projectId &&
|
|
72
|
+
currentProject &&
|
|
73
|
+
!sameProject(pointer.projectId, currentProject)) {
|
|
74
|
+
return refused(`cross-project resume refused: session belongs to '${pointer.projectId}', current directory resolves to '${currentProject}'`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (options.requestedProject &&
|
|
78
|
+
pointer.projectId &&
|
|
79
|
+
!sameProject(options.requestedProject, pointer.projectId)) {
|
|
80
|
+
return refused(`cross-project resume refused: session belongs to '${pointer.projectId}', request targeted '${options.requestedProject}'`);
|
|
81
|
+
}
|
|
82
|
+
const now = options.now ?? Date.now();
|
|
83
|
+
const age = now - Date.parse(session.updatedAt);
|
|
84
|
+
if (session.status === "created") {
|
|
85
|
+
return refused(`session ${identifier.value} never started (status: created) — nothing to resume`);
|
|
86
|
+
}
|
|
87
|
+
if (session.status === "failed" || session.status === "cancelled") {
|
|
88
|
+
return refused(`session ${identifier.value} ended in terminal status '${session.status}' — a new session must be started explicitly, Ocean will not reassign it automatically`);
|
|
89
|
+
}
|
|
90
|
+
if (session.status === "running" &&
|
|
91
|
+
Number.isFinite(age) &&
|
|
92
|
+
age > STALE_RESUME_MS) {
|
|
93
|
+
return refused(`session ${identifier.value} is stale: last updated ${Math.floor(age / 3_600_000)}h ago while still marked running — run 'ocean session doctor' before resuming`, ["stale-session"]);
|
|
94
|
+
}
|
|
95
|
+
const mode = session.status === "running" ? "attach-child" : "pointer-only";
|
|
96
|
+
const plan = {
|
|
97
|
+
ok: true,
|
|
98
|
+
mode,
|
|
99
|
+
reason: mode === "attach-child"
|
|
100
|
+
? `session ${identifier.value} is live; a child session may attach to it`
|
|
101
|
+
: `session ${identifier.value} is closed cleanly; a new child session may point at its checkpoint without replaying it`,
|
|
102
|
+
pointer,
|
|
103
|
+
parentSessionId: identifier.value,
|
|
104
|
+
violations: [],
|
|
105
|
+
};
|
|
106
|
+
const bounded = budget;
|
|
107
|
+
const serialized = JSON.stringify(plan).length;
|
|
108
|
+
if (serialized > bounded.maxChars) {
|
|
109
|
+
return refused(`resume plan is ${serialized} chars, budget.maxChars allows ${bounded.maxChars}`, ["max-chars-exceeded"]);
|
|
110
|
+
}
|
|
111
|
+
return plan;
|
|
112
|
+
}
|
|
113
|
+
// Validates an explicitly supplied parent before a caller passes it to store.create().
|
|
114
|
+
// A missing parent is reported as "none", never inferred from the most recent session.
|
|
115
|
+
export function validateParentSession(store, parentSessionId, childSessionId) {
|
|
116
|
+
if (parentSessionId === null || parentSessionId === undefined) {
|
|
117
|
+
return { ok: true, parentSessionId: null };
|
|
118
|
+
}
|
|
119
|
+
const identifier = validateSessionIdentifier(parentSessionId);
|
|
120
|
+
if (!identifier.valid)
|
|
121
|
+
return { ok: false, reason: identifier.reason };
|
|
122
|
+
if (childSessionId && childSessionId === identifier.value) {
|
|
123
|
+
return { ok: false, reason: "a session cannot be its own parent" };
|
|
124
|
+
}
|
|
125
|
+
const parent = store.get(identifier.value);
|
|
126
|
+
if (!parent)
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: `parent session ${identifier.value} does not exist in Ocean`,
|
|
130
|
+
};
|
|
131
|
+
return { ok: true, parentSessionId: identifier.value };
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=session-pointer.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { folderProject } from "@ocean/core/shared/project-resolution.js";
|
|
2
|
+
// The project to record on a new session. Recording a session must not fail on registry state,
|
|
3
|
+
// so an unreadable registry yields null; the next `ocean migrate --apply` fills it.
|
|
4
|
+
export async function sessionProjectId(cwd) {
|
|
5
|
+
try {
|
|
6
|
+
return await folderProject(cwd);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// Fill project_id on sessions recorded before the column existed, then on the handoffs that
|
|
13
|
+
// came from them. An unreadable registry throws and aborts: never guess a folder's project.
|
|
14
|
+
export async function backfillSessionProjects(store) {
|
|
15
|
+
let sessions = 0;
|
|
16
|
+
let unresolved = 0;
|
|
17
|
+
for (const directory of store.sessionDirectoriesMissingProject()) {
|
|
18
|
+
const project = await folderProject(directory);
|
|
19
|
+
if (project)
|
|
20
|
+
sessions += store.setProjectForDirectory(directory, project);
|
|
21
|
+
else
|
|
22
|
+
unresolved += 1;
|
|
23
|
+
}
|
|
24
|
+
return { sessions, handoffs: store.backfillHandoffProjects(), unresolved };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=session-projects.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeSessionSummary } from "@ocean/brain/memory/session-summary.js";
|
|
5
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
6
|
+
// Session retention (T-228): summarize finished, unsummarized sessions to markdown, then
|
|
7
|
+
// prune their raw provider_output/terminal_input events (never a session row, a handoff, an
|
|
8
|
+
// idea, or a capture-referenced event), then checkpoint+VACUUM. Default mode is dry-run
|
|
9
|
+
// (planRetention only); simulate measures a VACUUMed copy; apply is fingerprint-gated and is
|
|
10
|
+
// never invoked against the live sessions.sqlite by this task — only against fixture
|
|
11
|
+
// databases in tests.
|
|
12
|
+
export const DEFAULT_KEEP_DAYS = 7;
|
|
13
|
+
function computeFingerprint(before, types, sessionIds, eventIds) {
|
|
14
|
+
const sorted = [...sessionIds].sort();
|
|
15
|
+
// Not Math.max(...eventIds): spreading tens of thousands of event ids as call arguments
|
|
16
|
+
// overflows the engine's argument-count limit (RangeError: Maximum call stack size
|
|
17
|
+
// exceeded) on a real corpus, even though small fixture-scale tests never hit it.
|
|
18
|
+
let maxEventId = 0;
|
|
19
|
+
for (const id of eventIds)
|
|
20
|
+
if (id > maxEventId)
|
|
21
|
+
maxEventId = id;
|
|
22
|
+
return createHash("sha256")
|
|
23
|
+
.update(JSON.stringify({
|
|
24
|
+
before,
|
|
25
|
+
types: [...types].sort(),
|
|
26
|
+
sessionIds: sorted,
|
|
27
|
+
eventCount: eventIds.length,
|
|
28
|
+
maxEventId,
|
|
29
|
+
}))
|
|
30
|
+
.digest("hex");
|
|
31
|
+
}
|
|
32
|
+
export function planRetention(store, options = {}) {
|
|
33
|
+
const keepDays = options.keepDays ?? DEFAULT_KEEP_DAYS;
|
|
34
|
+
const before = options.before ??
|
|
35
|
+
new Date((options.now ?? Date.now()) - keepDays * 24 * 60 * 60 * 1000).toISOString();
|
|
36
|
+
const sessions = store.retentionEligibleSessions(before);
|
|
37
|
+
const sessionIds = sessions.map((session) => session.sessionId);
|
|
38
|
+
const needSummary = sessions
|
|
39
|
+
.filter((session) => !session.summaryPath)
|
|
40
|
+
.map((session) => session.sessionId);
|
|
41
|
+
const eventIds = store.retentionPrunableEventIds(sessionIds);
|
|
42
|
+
const dataBytes = store.eventsDataBytes(eventIds);
|
|
43
|
+
const RAW_EVENT_TYPES = ["provider_output", "terminal_input"];
|
|
44
|
+
return {
|
|
45
|
+
before,
|
|
46
|
+
keepDays,
|
|
47
|
+
sessionIds,
|
|
48
|
+
needSummary,
|
|
49
|
+
eventIds,
|
|
50
|
+
dataBytes,
|
|
51
|
+
fingerprint: computeFingerprint(before, RAW_EVENT_TYPES, sessionIds, eventIds),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Measures the effect of applying `plan` against a throwaway VACUUMed COPY of the database
|
|
55
|
+
// file backing `store` — the live file is never touched. The copy lives under a private,
|
|
56
|
+
// mode-0700 directory (never /tmp), and is always removed, even on error.
|
|
57
|
+
export async function simulateRetention(store, _sourceFile, plan, openStore) {
|
|
58
|
+
const simDir = oceanPath(SYSTEM_DIR, "sessions", ".compaction-sim");
|
|
59
|
+
await mkdir(simDir, { recursive: true, mode: 0o700 });
|
|
60
|
+
const copyPath = path.join(simDir, `sim-${process.pid}-${Date.now()}.sqlite`);
|
|
61
|
+
await rm(copyPath, { force: true });
|
|
62
|
+
try {
|
|
63
|
+
await store.backupTo(copyPath);
|
|
64
|
+
const beforeBytes = (await stat(copyPath)).size;
|
|
65
|
+
const copyStore = openStore(copyPath);
|
|
66
|
+
try {
|
|
67
|
+
copyStore.deleteEvents(plan.eventIds);
|
|
68
|
+
copyStore.checkpointAndVacuum();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
copyStore.close();
|
|
72
|
+
}
|
|
73
|
+
const afterBytes = (await stat(copyPath)).size;
|
|
74
|
+
return {
|
|
75
|
+
beforeBytes,
|
|
76
|
+
afterBytes,
|
|
77
|
+
freedBytes: beforeBytes - afterBytes,
|
|
78
|
+
plan,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await rm(copyPath, { force: true });
|
|
83
|
+
await rm(`${copyPath}-wal`, { force: true });
|
|
84
|
+
await rm(`${copyPath}-shm`, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Fingerprint-gated: refuses to run if `fingerprint` does not match a freshly recomputed
|
|
88
|
+
// plan for the same `before`/keepDays — the database must not have moved out from under the
|
|
89
|
+
// plan between planning and applying. Backs up first, summarizes any unsummarized finished
|
|
90
|
+
// session (verifying the written file's hash before trusting it), deletes only the planned
|
|
91
|
+
// event ids, appends one audit event per session, then checkpoints+VACUUMs.
|
|
92
|
+
export async function applyRetention(store, sourceFile, plan, fingerprint) {
|
|
93
|
+
const fresh = planRetention(store, {
|
|
94
|
+
keepDays: plan.keepDays,
|
|
95
|
+
before: plan.before,
|
|
96
|
+
});
|
|
97
|
+
if (fresh.fingerprint !== fingerprint ||
|
|
98
|
+
fresh.fingerprint !== plan.fingerprint) {
|
|
99
|
+
throw new Error("retention plan fingerprint mismatch — the database changed since this plan was made; refusing to apply a stale plan");
|
|
100
|
+
}
|
|
101
|
+
const backupDir = oceanPath(SYSTEM_DIR, "sessions", ".backups");
|
|
102
|
+
await mkdir(backupDir, { recursive: true, mode: 0o700 });
|
|
103
|
+
const backupPath = path.join(backupDir, `sessions-${Date.now()}.sqlite`);
|
|
104
|
+
await store.backupTo(backupPath);
|
|
105
|
+
const beforeBytes = (await stat(sourceFile)).size;
|
|
106
|
+
const summarized = [];
|
|
107
|
+
for (const sessionId of plan.needSummary) {
|
|
108
|
+
const session = store.get(sessionId);
|
|
109
|
+
if (!session)
|
|
110
|
+
continue;
|
|
111
|
+
const events = store.listEvents(sessionId);
|
|
112
|
+
const written = await writeSessionSummary({ session, events });
|
|
113
|
+
store.setSummary(sessionId, written);
|
|
114
|
+
summarized.push(sessionId);
|
|
115
|
+
}
|
|
116
|
+
store.deleteEvents(plan.eventIds);
|
|
117
|
+
// Retention's audit event is best-effort and coarse (the total pruned count for the whole
|
|
118
|
+
// plan, not a precise per-session breakdown), recorded once against every planned session.
|
|
119
|
+
for (const sessionId of plan.sessionIds) {
|
|
120
|
+
store.recordRetentionPrune(sessionId, plan.eventIds.length, plan.before);
|
|
121
|
+
}
|
|
122
|
+
store.checkpointAndVacuum();
|
|
123
|
+
const afterBytes = (await stat(sourceFile)).size;
|
|
124
|
+
return {
|
|
125
|
+
backupPath,
|
|
126
|
+
summarized,
|
|
127
|
+
eventsDeleted: plan.eventIds.length,
|
|
128
|
+
beforeBytes,
|
|
129
|
+
afterBytes,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=session-retention.js.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { access, mkdir, readdir, readFile, rename, rmdir, writeFile, } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithin, WORKSPACE_PROJECT_ID, workspaceTasksRoot, } from "@ocean/core/shared/paths.js";
|
|
4
|
+
import { projectArchiveName, WORKSPACE_ARCHIVE_NAMESPACE, } from "@ocean/core/shared/task-reference.js";
|
|
5
|
+
import { verifyTask } from "./verify-task.js";
|
|
6
|
+
const field = (source, name) => source
|
|
7
|
+
.match(new RegExp(`^${name}:\\s*(.+)$`, "m"))?.[1]
|
|
8
|
+
?.trim()
|
|
9
|
+
.replace(/^['"]|['"]$/g, "") ?? "";
|
|
10
|
+
function setState(source, state) {
|
|
11
|
+
return source.replace(/^state:\s*.+$/m, `state: ${state}`);
|
|
12
|
+
}
|
|
13
|
+
function section(source, heading) {
|
|
14
|
+
const header = new RegExp(`^## ${heading}[ \\t]*\\r?$`, "m").exec(source);
|
|
15
|
+
if (!header)
|
|
16
|
+
return undefined;
|
|
17
|
+
const start = source.indexOf("\n", header.index) + 1;
|
|
18
|
+
const nextHeading = /^## /m.exec(source.slice(start));
|
|
19
|
+
return source.slice(start, nextHeading ? start + nextHeading.index : source.length);
|
|
20
|
+
}
|
|
21
|
+
function completionIssues(source) {
|
|
22
|
+
const issues = [];
|
|
23
|
+
if (/^\s*-\s+"?\[ \] /m.test(source))
|
|
24
|
+
issues.push("unchecked work");
|
|
25
|
+
const blockers = section(source, "Blockers");
|
|
26
|
+
if (blockers === undefined) {
|
|
27
|
+
issues.push("missing Blockers section");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const entries = blockers
|
|
31
|
+
.split("\n")
|
|
32
|
+
.map((line) => line.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
const open = entries.filter((line) => {
|
|
35
|
+
if (/^(?:[-*]\s*)?(?:none|no blockers?)\.?$/i.test(line))
|
|
36
|
+
return false;
|
|
37
|
+
const checkbox = line.match(/^[-*]\s+\[([ xX])\]\s+.+$/);
|
|
38
|
+
if (checkbox)
|
|
39
|
+
return checkbox[1].toLowerCase() !== "x";
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
if (open.length)
|
|
43
|
+
issues.push("unresolved blockers");
|
|
44
|
+
}
|
|
45
|
+
const definition = section(source, "Definition of done");
|
|
46
|
+
if (definition === undefined) {
|
|
47
|
+
issues.push("missing Definition of done section");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const criteria = definition
|
|
51
|
+
.split("\n")
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
if (!criteria.length) {
|
|
55
|
+
issues.push("missing acceptance criteria");
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const unchecked = criteria.some((line) => {
|
|
59
|
+
const checkbox = line.match(/^[-*]\s+\[([ xX])\]\s+.+$/);
|
|
60
|
+
if (!checkbox)
|
|
61
|
+
return true;
|
|
62
|
+
return checkbox[1].toLowerCase() !== "x";
|
|
63
|
+
});
|
|
64
|
+
const malformed = criteria.some((line) => !/^[-*]\s+\[([ xX])\]\s+.+$/.test(line));
|
|
65
|
+
if (malformed)
|
|
66
|
+
issues.push("acceptance criteria must use checked boxes");
|
|
67
|
+
else if (unchecked)
|
|
68
|
+
issues.push("unchecked acceptance criteria");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return issues;
|
|
72
|
+
}
|
|
73
|
+
export async function completeTask(id, root = workspaceTasksRoot(), options = {}) {
|
|
74
|
+
const taskFile = resolveWithin(root, id, "task.md");
|
|
75
|
+
const source = await readFile(taskFile, "utf8");
|
|
76
|
+
const issues = completionIssues(source);
|
|
77
|
+
if (issues.length)
|
|
78
|
+
throw new Error(`Cannot complete ${id}: ${issues.join("; ")}`);
|
|
79
|
+
const outcome = await verifyTask(id, root, {
|
|
80
|
+
cwd: options.cwd,
|
|
81
|
+
run: options.run,
|
|
82
|
+
});
|
|
83
|
+
if (!outcome.passed)
|
|
84
|
+
throw new Error(`Cannot complete ${id}: ${outcome.reason}`);
|
|
85
|
+
if (field(source, "state") !== "done")
|
|
86
|
+
await writeFile(taskFile, setState(source, "done"));
|
|
87
|
+
const archived = await archiveDoneTasks(root, true, {
|
|
88
|
+
...options,
|
|
89
|
+
verified: new Set([id]),
|
|
90
|
+
only: new Set([id]),
|
|
91
|
+
});
|
|
92
|
+
const skipped = archived.skipped.find((entry) => entry.id === id);
|
|
93
|
+
if (skipped) {
|
|
94
|
+
await writeFile(taskFile, source, "utf8");
|
|
95
|
+
throw new Error(`Cannot complete ${id}: ${skipped.reason}`);
|
|
96
|
+
}
|
|
97
|
+
return { id, state: "done", ...archived };
|
|
98
|
+
}
|
|
99
|
+
export async function archiveDoneTasks(root = workspaceTasksRoot(), apply = false, options = {}) {
|
|
100
|
+
const result = {
|
|
101
|
+
candidates: [],
|
|
102
|
+
skipped: [],
|
|
103
|
+
moved: [],
|
|
104
|
+
repaired: [],
|
|
105
|
+
};
|
|
106
|
+
const archiveRoot = resolveWithin(root, "archive");
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === "ENOENT")
|
|
113
|
+
return result;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (!entry.isDirectory() || entry.name === "archive")
|
|
118
|
+
continue;
|
|
119
|
+
const sourceDirectory = resolveWithin(root, entry.name);
|
|
120
|
+
const taskFile = resolveWithin(root, entry.name, "task.md");
|
|
121
|
+
try {
|
|
122
|
+
await access(taskFile);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
const archivedDirectory = resolveWithin(archiveRoot, WORKSPACE_ARCHIVE_NAMESPACE, entry.name);
|
|
126
|
+
try {
|
|
127
|
+
await access(path.join(archivedDirectory, "task.md"));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue; // not archived
|
|
131
|
+
}
|
|
132
|
+
if (!apply)
|
|
133
|
+
continue;
|
|
134
|
+
for (const artifact of await readdir(sourceDirectory, {
|
|
135
|
+
withFileTypes: true,
|
|
136
|
+
})) {
|
|
137
|
+
const destination = resolveWithin(archivedDirectory, artifact.name);
|
|
138
|
+
try {
|
|
139
|
+
await access(destination);
|
|
140
|
+
result.skipped.push({
|
|
141
|
+
id: entry.name,
|
|
142
|
+
reason: `archive artifact exists: ${artifact.name}`,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code !== "ENOENT")
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
await rename(path.join(sourceDirectory, artifact.name), destination);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await rmdir(sourceDirectory);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code !== "ENOTEMPTY")
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
result.repaired.push(entry.name);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const source = await readFile(taskFile, "utf8");
|
|
163
|
+
const id = field(source, "id") || entry.name;
|
|
164
|
+
const state = field(source, "state");
|
|
165
|
+
if (state !== "done" && state !== "cancelled")
|
|
166
|
+
continue;
|
|
167
|
+
if (options.only && !options.only.has(id))
|
|
168
|
+
continue;
|
|
169
|
+
if (state === "done") {
|
|
170
|
+
const issues = completionIssues(source);
|
|
171
|
+
if (issues.length) {
|
|
172
|
+
result.skipped.push({ id, reason: issues.join("; ") });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (apply && !options.verified?.has(id)) {
|
|
176
|
+
const outcome = await verifyTask(id, root, {
|
|
177
|
+
cwd: options.cwd,
|
|
178
|
+
run: options.run,
|
|
179
|
+
});
|
|
180
|
+
if (!outcome.passed) {
|
|
181
|
+
result.skipped.push({ id, reason: outcome.reason });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (state === "cancelled" && source.match(/^\s*-\s+"?\[ \] /m)) {
|
|
187
|
+
result.skipped.push({ id, reason: "unchecked work" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
result.candidates.push(id);
|
|
191
|
+
if (!apply)
|
|
192
|
+
continue;
|
|
193
|
+
const project = field(source, "project") || WORKSPACE_PROJECT_ID;
|
|
194
|
+
const destinationDirectory = resolveWithin(archiveRoot, projectArchiveName(project), id);
|
|
195
|
+
try {
|
|
196
|
+
await access(destinationDirectory);
|
|
197
|
+
result.skipped.push({ id, reason: "archive destination exists" });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error.code !== "ENOENT")
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
await mkdir(path.dirname(destinationDirectory), { recursive: true });
|
|
205
|
+
await rename(sourceDirectory, destinationDirectory);
|
|
206
|
+
result.moved.push(id);
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=archive-tasks.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
3
|
+
import { resolveWithin, workspaceTasksRoot } from "@ocean/core/shared/paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* Append a durable checkpoint to a task's `## Log` and, when given, replace its
|
|
6
|
+
* `## Next action`. Writing to the task file means the checkpoint survives a
|
|
7
|
+
* cold restart: a later loop iteration or a fresh session reads it back from
|
|
8
|
+
* disk rather than from in-memory state.
|
|
9
|
+
*/
|
|
10
|
+
export async function checkpointTask(id, root = workspaceTasksRoot(), input) {
|
|
11
|
+
if (!input.note?.trim())
|
|
12
|
+
throw new Error("Checkpoint requires a note.");
|
|
13
|
+
const taskFile = resolveWithin(root, id, "task.md");
|
|
14
|
+
let source = await readFile(taskFile, "utf8");
|
|
15
|
+
const date = input.date ?? localDate();
|
|
16
|
+
const entry = `- ${date} — ${input.note.trim()}`;
|
|
17
|
+
if (/^## Log\s*$/m.test(source))
|
|
18
|
+
source = source.replace(/^## Log\s*$/m, `## Log\n\n${entry}`);
|
|
19
|
+
else
|
|
20
|
+
source = `${source.trimEnd()}\n\n## Log\n\n${entry}\n`;
|
|
21
|
+
if (input.next?.trim()) {
|
|
22
|
+
const next = input.next.trim();
|
|
23
|
+
if (/^## Next action\s*$/m.test(source))
|
|
24
|
+
source = source.replace(/^## Next action\s*$[\s\S]*?(?=^## |\s*$)/m, `## Next action\n\n${next}\n\n`);
|
|
25
|
+
else
|
|
26
|
+
source = `${source.trimEnd()}\n\n## Next action\n\n${next}\n`;
|
|
27
|
+
}
|
|
28
|
+
await writeFile(taskFile, source, "utf8");
|
|
29
|
+
return { checkpointed: id };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=checkpoint-task.js.map
|