@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,126 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
3
|
+
import { resolveWithin } from "@ocean/core/shared/paths.js";
|
|
4
|
+
// Keep equal to CLAIM_TTL_MS in scripts/validate-tasks.mjs (a test pins the two together).
|
|
5
|
+
export const CLAIM_TTL_MS = 2 * 60 * 60 * 1000;
|
|
6
|
+
const OWNER = /^[\w.@/-]{1,64}$/;
|
|
7
|
+
const CLAIM_PATH = /^(?!\/)(?!.*\.\.)[\w.@/-]+$/;
|
|
8
|
+
const unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
9
|
+
function frontmatterFields(source) {
|
|
10
|
+
const end = source.indexOf("\n---", 4);
|
|
11
|
+
if (!source.startsWith("---\n") || end < 0)
|
|
12
|
+
return {};
|
|
13
|
+
const fields = {};
|
|
14
|
+
for (const line of source.slice(4, end).split("\n")) {
|
|
15
|
+
const separator = line.indexOf(":");
|
|
16
|
+
if (separator > 0 && /^[\w-]+$/.test(line.slice(0, separator)))
|
|
17
|
+
fields[line.slice(0, separator)] = line.slice(separator + 1).trim();
|
|
18
|
+
}
|
|
19
|
+
return fields;
|
|
20
|
+
}
|
|
21
|
+
function parseClaim(fields) {
|
|
22
|
+
if (!fields.claimed_by || !fields.claimed_at)
|
|
23
|
+
return null;
|
|
24
|
+
const list = fields.claimed_paths?.match(/^\[(.*)\]$/)?.[1] ?? "";
|
|
25
|
+
return {
|
|
26
|
+
by: unquote(fields.claimed_by),
|
|
27
|
+
at: unquote(fields.claimed_at),
|
|
28
|
+
paths: list
|
|
29
|
+
.split(",")
|
|
30
|
+
.map((entry) => normalizePath(unquote(entry.trim())))
|
|
31
|
+
.filter(Boolean),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const normalizePath = (value) => value.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
35
|
+
const overlaps = (a, b) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
36
|
+
export function isStale(claim, now = new Date()) {
|
|
37
|
+
const claimedAt = Date.parse(claim.at);
|
|
38
|
+
return Number.isNaN(claimedAt) || now.getTime() - claimedAt > CLAIM_TTL_MS;
|
|
39
|
+
}
|
|
40
|
+
// Replace the claim lines in the frontmatter (and bump `updated`), keeping every other line.
|
|
41
|
+
function withClaim(source, claim, today) {
|
|
42
|
+
const end = source.indexOf("\n---", 4);
|
|
43
|
+
const kept = source
|
|
44
|
+
.slice(4, end)
|
|
45
|
+
.split("\n")
|
|
46
|
+
.filter((line) => !/^claimed_(by|at|paths):/.test(line))
|
|
47
|
+
.map((line) => (/^updated:/.test(line) ? `updated: ${today}` : line));
|
|
48
|
+
if (claim) {
|
|
49
|
+
kept.push(`claimed_by: ${claim.by}`, `claimed_at: ${claim.at}`);
|
|
50
|
+
if (claim.paths.length)
|
|
51
|
+
kept.push(`claimed_paths: [${claim.paths.join(", ")}]`);
|
|
52
|
+
}
|
|
53
|
+
return `---\n${kept.join("\n")}${source.slice(end)}`;
|
|
54
|
+
}
|
|
55
|
+
async function liveClaimsElsewhere(root, id, now) {
|
|
56
|
+
const found = [];
|
|
57
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
58
|
+
if (!entry.isDirectory() || entry.name === "archive" || entry.name === id)
|
|
59
|
+
continue;
|
|
60
|
+
const source = await readFile(resolveWithin(root, entry.name, "task.md"), "utf8").catch(() => "");
|
|
61
|
+
const claim = parseClaim(frontmatterFields(source));
|
|
62
|
+
if (claim && !isStale(claim, now))
|
|
63
|
+
found.push({ id: entry.name, claim });
|
|
64
|
+
}
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Record that `by` is working on a task. Refuses (throws, naming the holder) when another
|
|
69
|
+
* owner holds a non-stale claim on it, or when `paths` overlap a non-stale claim another owner
|
|
70
|
+
* holds on a different task. The same owner re-claiming refreshes the timestamp. Claims are
|
|
71
|
+
* advisory: they make a collision visible, they do not lock the files.
|
|
72
|
+
*/
|
|
73
|
+
export async function claimTask(id, root, input) {
|
|
74
|
+
if (!OWNER.test(input.by))
|
|
75
|
+
throw new Error("--by must be 1-64 chars of letters, digits, . _ @ / -");
|
|
76
|
+
const paths = (input.paths ?? []).map(normalizePath).filter(Boolean);
|
|
77
|
+
for (const entry of paths)
|
|
78
|
+
if (!CLAIM_PATH.test(entry))
|
|
79
|
+
throw new Error(`Invalid claim path: ${entry}`);
|
|
80
|
+
const now = input.now ?? new Date();
|
|
81
|
+
const taskFile = resolveWithin(root, id, "task.md");
|
|
82
|
+
const source = await readFile(taskFile, "utf8");
|
|
83
|
+
const fields = frontmatterFields(source);
|
|
84
|
+
if (["done", "cancelled"].includes(fields.state))
|
|
85
|
+
throw new Error(`${id} is ${fields.state}; it cannot be claimed`);
|
|
86
|
+
const held = parseClaim(fields);
|
|
87
|
+
let tookOver;
|
|
88
|
+
if (held && held.by !== input.by && !isStale(held, now)) {
|
|
89
|
+
if (!input.force)
|
|
90
|
+
throw new Error(`${id} is claimed by ${held.by} since ${held.at}. Ask the owner, wait for it to expire (2 h), or use --force.`);
|
|
91
|
+
tookOver = held.by;
|
|
92
|
+
}
|
|
93
|
+
if (!input.force)
|
|
94
|
+
for (const other of await liveClaimsElsewhere(root, id, now)) {
|
|
95
|
+
if (other.claim.by === input.by)
|
|
96
|
+
continue;
|
|
97
|
+
const clash = paths.find((mine) => other.claim.paths.some((theirs) => overlaps(mine, theirs)));
|
|
98
|
+
if (clash)
|
|
99
|
+
throw new Error(`${clash} overlaps ${other.claim.paths.join(", ")} claimed by ${other.claim.by} on ${other.id}.`);
|
|
100
|
+
}
|
|
101
|
+
const claim = { by: input.by, at: now.toISOString(), paths };
|
|
102
|
+
await writeFile(taskFile, withClaim(source, claim, localDate(now)), "utf8");
|
|
103
|
+
// Read-modify-write is not atomic: if a concurrent claim landed between our read and write,
|
|
104
|
+
// the file now names someone else. Re-read so the loser finds out instead of assuming it won.
|
|
105
|
+
const written = parseClaim(frontmatterFields(await readFile(taskFile, "utf8")));
|
|
106
|
+
if (written?.by !== input.by)
|
|
107
|
+
throw new Error(`${id} was claimed by ${written?.by} at the same time.`);
|
|
108
|
+
return { claimed: id, by: claim.by, at: claim.at, tookOver };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Drop a task's claim. Only the owner may release a live claim (or `force`); a stale claim can
|
|
112
|
+
* be released by anyone. Returns `released: false` when there was nothing to release.
|
|
113
|
+
*/
|
|
114
|
+
export async function releaseTask(id, root, input) {
|
|
115
|
+
const now = input.now ?? new Date();
|
|
116
|
+
const taskFile = resolveWithin(root, id, "task.md");
|
|
117
|
+
const source = await readFile(taskFile, "utf8");
|
|
118
|
+
const held = parseClaim(frontmatterFields(source));
|
|
119
|
+
if (!held)
|
|
120
|
+
return { released: false, id };
|
|
121
|
+
if (held.by !== input.by && !isStale(held, now) && !input.force)
|
|
122
|
+
throw new Error(`${id} is claimed by ${held.by}; only that owner can release it (or use --force).`);
|
|
123
|
+
await writeFile(taskFile, withClaim(source, null, localDate(now)), "utf8");
|
|
124
|
+
return { released: true, id, was: held.by };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=claim-task.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseTaskId, taskIdSpellings, } from "@ocean/core/domain/tasks/task-id.js";
|
|
4
|
+
import { oceanPath, PROJECTS_DIR, projectSubdir, resolveWithin, WORKSPACE_PROJECT_ID, } from "@ocean/core/shared/paths.js";
|
|
5
|
+
import { folderProject, projectsWithTasks, } from "@ocean/core/shared/project-resolution.js";
|
|
6
|
+
/**
|
|
7
|
+
* Read task summaries for a project from the workspace. Lives in the application
|
|
8
|
+
* layer so both the CLI (`ocean tasks list`) and the MCP server can call it
|
|
9
|
+
* without the MCP server reaching back into the CLI layer.
|
|
10
|
+
*/
|
|
11
|
+
export async function listTasks(state, projectId = WORKSPACE_PROJECT_ID) {
|
|
12
|
+
const root = resolveWithin(oceanPath(PROJECTS_DIR), projectSubdir(oceanPath(PROJECTS_DIR), projectId), "tasks");
|
|
13
|
+
const records = [];
|
|
14
|
+
let entries;
|
|
15
|
+
try {
|
|
16
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return records;
|
|
20
|
+
}
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (!entry.isDirectory() || entry.name === "archive")
|
|
23
|
+
continue;
|
|
24
|
+
const file = path.join(root, entry.name, "task.md");
|
|
25
|
+
let source;
|
|
26
|
+
try {
|
|
27
|
+
source = await readFile(file, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const fields = Object.fromEntries(source
|
|
33
|
+
.slice(0, source.indexOf("\n---", 4))
|
|
34
|
+
.split("\n")
|
|
35
|
+
.slice(1)
|
|
36
|
+
.filter((line) => /^[\w-]+:/.test(line))
|
|
37
|
+
.map((line) => {
|
|
38
|
+
const index = line.indexOf(":");
|
|
39
|
+
const value = line.slice(index + 1).trim();
|
|
40
|
+
return [line.slice(0, index), value.replace(/^(["'])(.*)\1$/, "$2")];
|
|
41
|
+
}));
|
|
42
|
+
if (state && fields.state !== state)
|
|
43
|
+
continue;
|
|
44
|
+
const objective = source.match(/\n## Objective\n\n([\s\S]*?)(?=\n## |$)/)?.[1]?.trim() ??
|
|
45
|
+
"";
|
|
46
|
+
records.push({
|
|
47
|
+
id: fields.id ?? entry.name,
|
|
48
|
+
title: fields.title ?? "",
|
|
49
|
+
state: fields.state ?? "",
|
|
50
|
+
goal: objective || fields.goal || fields.requirement || "",
|
|
51
|
+
updatedAt: fields.updated ?? fields.updated_at ?? "",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
55
|
+
}
|
|
56
|
+
// Projects whose live tasks/ holds this id (T-3 and T-003 are one task). Ids are unique per
|
|
57
|
+
// project, not across projects.
|
|
58
|
+
export async function projectsHoldingTask(id) {
|
|
59
|
+
if (!parseTaskId(id))
|
|
60
|
+
return [];
|
|
61
|
+
const holders = [];
|
|
62
|
+
for (const project of await projectsWithTasks()) {
|
|
63
|
+
for (const spelling of taskIdSpellings(id)) {
|
|
64
|
+
try {
|
|
65
|
+
await readFile(path.join(oceanPath(PROJECTS_DIR), projectSubdir(oceanPath(PROJECTS_DIR), project), "tasks", spelling, "task.md"), "utf8");
|
|
66
|
+
holders.push(project);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* not here */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return holders;
|
|
75
|
+
}
|
|
76
|
+
// "What tasks do I have" from a folder (T-256): --project wins; else the folder's project when
|
|
77
|
+
// it has matching tasks; else every project's, labelled, with a note saying why.
|
|
78
|
+
export async function listTasksForFolder(options) {
|
|
79
|
+
const { state } = options;
|
|
80
|
+
const label = (project, tasks) => tasks.map((task) => ({ ...task, project }));
|
|
81
|
+
if (options.project) {
|
|
82
|
+
// A mistyped id would otherwise read as a project with no tasks.
|
|
83
|
+
// resolveWithin throws on its own for an id that escapes the projects root.
|
|
84
|
+
const folder = resolveWithin(oceanPath(PROJECTS_DIR), projectSubdir(oceanPath(PROJECTS_DIR), options.project));
|
|
85
|
+
const isProject = await stat(folder).then((entry) => entry.isDirectory(), () => false);
|
|
86
|
+
if (!isProject)
|
|
87
|
+
throw new Error(`No project ${options.project} in ${PROJECTS_DIR}`);
|
|
88
|
+
return {
|
|
89
|
+
scope: "project",
|
|
90
|
+
project: options.project,
|
|
91
|
+
tasks: label(options.project, await listTasks(state, options.project)),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const project = await folderProject(options.cwd);
|
|
95
|
+
if (project) {
|
|
96
|
+
const own = await listTasks(state, project);
|
|
97
|
+
if (own.length)
|
|
98
|
+
return { scope: "project", project, tasks: label(project, own) };
|
|
99
|
+
}
|
|
100
|
+
const tasks = [];
|
|
101
|
+
for (const each of await projectsWithTasks())
|
|
102
|
+
tasks.push(...label(each, await listTasks(state, each)));
|
|
103
|
+
const which = state ? `${state} ` : "";
|
|
104
|
+
return {
|
|
105
|
+
scope: "all",
|
|
106
|
+
project,
|
|
107
|
+
tasks,
|
|
108
|
+
note: project
|
|
109
|
+
? `No ${which}tasks in ${project}; showing every project.`
|
|
110
|
+
: `This folder is not an Ocean project; showing every project's ${which}tasks.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=list-tasks.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
4
|
+
import { oceanPath, PROJECTS_DIR, projectSubdir, resolveWithin, } from "@ocean/core/shared/paths.js";
|
|
5
|
+
import { withNextTaskId } from "@ocean/core/shared/project-resolution.js";
|
|
6
|
+
// A new record in the shape `tasks doctor` validates and `tasks complete` can close: the
|
|
7
|
+
// definition of done is a checkbox, so it can be ticked rather than rewritten first.
|
|
8
|
+
export function taskRecord(input) {
|
|
9
|
+
const { id, title, project, objective, today } = input;
|
|
10
|
+
return `---\nid: ${id}\ntitle: ${JSON.stringify(title)}\nstate: active\nproject: ${project}\nopened: ${today}\nupdated: ${today}\nartifacts: []\nclass: medium\nexpected_context: medium\n---\n\n## Objective\n\n${objective}\n\n## Definition of done\n\n- [ ] The requested outcome is implemented and its verification passes.\n\n## Next action\n\nInspect the project and define the first implementation slice.\n\n## Verification\n\nPending.\n\n## Blockers\n\nNone.\n\n## Log\n\n- ${today} — Task created.\n`;
|
|
11
|
+
}
|
|
12
|
+
export function validTaskTitle(title) {
|
|
13
|
+
const trimmed = title?.trim();
|
|
14
|
+
return Boolean(trimmed && trimmed.length <= 200 && !/[\r\n]/.test(trimmed));
|
|
15
|
+
}
|
|
16
|
+
// `ocean tasks new`: the next free id comes from the projects lock (the highest id on disk,
|
|
17
|
+
// live or archived, and the manifest counter), never from a hand-kept number.
|
|
18
|
+
export async function newTask(projectId, title) {
|
|
19
|
+
if (!validTaskTitle(title))
|
|
20
|
+
throw new Error("A task needs a single-line title of at most 200 characters");
|
|
21
|
+
// A mistyped project would otherwise get a fresh folder and an id from the shared namespace.
|
|
22
|
+
const folder = resolveWithin(oceanPath(PROJECTS_DIR), projectSubdir(oceanPath(PROJECTS_DIR), projectId));
|
|
23
|
+
const isProject = await stat(folder).then((entry) => entry.isDirectory(), () => false);
|
|
24
|
+
if (!isProject)
|
|
25
|
+
throw new Error(`No project ${projectId} in ${PROJECTS_DIR}`);
|
|
26
|
+
const today = localDate();
|
|
27
|
+
const result = await withNextTaskId(projectId, async (id) => {
|
|
28
|
+
const file = resolveWithin(oceanPath(PROJECTS_DIR), projectSubdir(oceanPath(PROJECTS_DIR), projectId), "tasks", id, "task.md");
|
|
29
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
30
|
+
// `wx`: an id that already has a record is never overwritten.
|
|
31
|
+
await writeFile(file, taskRecord({
|
|
32
|
+
id,
|
|
33
|
+
title: title.trim(),
|
|
34
|
+
project: projectId,
|
|
35
|
+
objective: title.trim(),
|
|
36
|
+
today,
|
|
37
|
+
}), { flag: "wx" });
|
|
38
|
+
return { ok: true, id, file };
|
|
39
|
+
});
|
|
40
|
+
return { id: result.id, file: result.file };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=new-task.js.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { access, readFile, rename } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { formatTaskId, isValidPrefix, LEGACY_PREFIX, parseTaskId, } from "@ocean/core/domain/tasks/task-id.js";
|
|
4
|
+
import { atomicWrite } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { isWorkspaceProject, oceanRoot, PROJECTS_DIR, WORKSPACE_PROJECT_ID, } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { highestTaskNumber, listProjectManifests, withProjectsLock, writeManifest, } from "@ocean/core/shared/project-resolution.js";
|
|
7
|
+
import { taskFolders } from "@ocean/core/shared/task-reference.js";
|
|
8
|
+
// Moves a project's tasks from the shared T namespace to its own prefix (T-256): T-300 becomes
|
|
9
|
+
// OL-001, and the record keeps `legacyId: T-300` so every old reference still resolves. Dry run
|
|
10
|
+
// unless `write`. Idempotent: a task already under the prefix keeps its number, so a rerun only
|
|
11
|
+
// picks up what is new.
|
|
12
|
+
export async function renumberTasks(options) {
|
|
13
|
+
const { project, prefix } = options;
|
|
14
|
+
if (isWorkspaceProject(project))
|
|
15
|
+
throw new Error(`${project} is the workspace project and keeps the ${LEGACY_PREFIX} namespace`);
|
|
16
|
+
if (prefix === LEGACY_PREFIX || !isValidPrefix(prefix))
|
|
17
|
+
throw new Error(`'${prefix}' is not a usable prefix: 1-6 upper-case letters or digits, starting with a letter, and not a reserved word`);
|
|
18
|
+
return withProjectsLock(async () => {
|
|
19
|
+
const manifests = await listProjectManifests();
|
|
20
|
+
const ocean = manifests.find((manifest) => manifest.id === WORKSPACE_PROJECT_ID);
|
|
21
|
+
if (!ocean)
|
|
22
|
+
throw new Error("no project.json for the workspace project; run `ocean project migrate --write` first");
|
|
23
|
+
const manifest = manifests.find((candidate) => candidate.id === project);
|
|
24
|
+
if (!manifest)
|
|
25
|
+
throw new Error(`no project.json for '${project}'; bind it before renumbering`);
|
|
26
|
+
const claimant = manifests.find((candidate) => candidate.prefix === prefix && candidate.id !== project);
|
|
27
|
+
if (claimant)
|
|
28
|
+
throw new Error(`prefix ${prefix} is already used by ${claimant.id}`);
|
|
29
|
+
if (manifest.prefix && manifest.prefix !== prefix)
|
|
30
|
+
throw new Error(`${project} already uses prefix ${manifest.prefix}`);
|
|
31
|
+
const projectsDir = path.join(oceanRoot(), PROJECTS_DIR);
|
|
32
|
+
const entries = [];
|
|
33
|
+
for (const [folder, file, archived] of await taskFolders(projectsDir, project)) {
|
|
34
|
+
let source;
|
|
35
|
+
try {
|
|
36
|
+
source = await readFile(file, "utf8");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const head = source.slice(0, Math.max(source.indexOf("\n---", 4), 0));
|
|
42
|
+
const field = (name) => head.match(new RegExp(`^${name}:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
43
|
+
const declared = parseTaskId(field("id") ?? "");
|
|
44
|
+
const oldId = field("legacyId") ?? folder;
|
|
45
|
+
if (!parseTaskId(oldId))
|
|
46
|
+
continue;
|
|
47
|
+
entries.push({
|
|
48
|
+
folder,
|
|
49
|
+
file,
|
|
50
|
+
archived,
|
|
51
|
+
oldId,
|
|
52
|
+
declared: declared?.prefix === prefix ? declared.number : null,
|
|
53
|
+
source,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Tasks already under the prefix keep their number; the rest are numbered after them, in
|
|
57
|
+
// the order of their old ids.
|
|
58
|
+
const kept = entries.filter((entry) => entry.declared !== null);
|
|
59
|
+
let counter = Math.max(0, ...kept.map((entry) => entry.declared ?? 0));
|
|
60
|
+
const pending = entries
|
|
61
|
+
.filter((entry) => entry.declared === null)
|
|
62
|
+
.sort((a, b) => (parseTaskId(a.oldId)?.number ?? 0) -
|
|
63
|
+
(parseTaskId(b.oldId)?.number ?? 0));
|
|
64
|
+
const targets = new Map();
|
|
65
|
+
for (const entry of kept)
|
|
66
|
+
targets.set(entry, formatTaskId(prefix, entry.declared));
|
|
67
|
+
for (const entry of pending) {
|
|
68
|
+
counter += 1;
|
|
69
|
+
targets.set(entry, formatTaskId(prefix, counter));
|
|
70
|
+
}
|
|
71
|
+
const moves = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const to = targets.get(entry);
|
|
74
|
+
if (to === entry.folder && entry.declared !== null)
|
|
75
|
+
continue;
|
|
76
|
+
moves.push({ ...entry, to });
|
|
77
|
+
}
|
|
78
|
+
moves.sort((a, b) => a.to.localeCompare(b.to));
|
|
79
|
+
for (const move of moves) {
|
|
80
|
+
const destination = path.join(path.dirname(path.dirname(move.file)), move.to);
|
|
81
|
+
if (move.to !== move.folder && (await exists(destination)))
|
|
82
|
+
throw new Error(`${path.relative(oceanRoot(), destination)} already exists; resolve it before renumbering`);
|
|
83
|
+
}
|
|
84
|
+
const nextTask = Math.max(manifest.nextTask ?? 1, counter + 1);
|
|
85
|
+
const result = {
|
|
86
|
+
dryRun: !options.write,
|
|
87
|
+
project,
|
|
88
|
+
prefix,
|
|
89
|
+
moves: moves.map(({ folder, to, archived }) => ({
|
|
90
|
+
from: folder,
|
|
91
|
+
to,
|
|
92
|
+
archived,
|
|
93
|
+
})),
|
|
94
|
+
nextTask,
|
|
95
|
+
};
|
|
96
|
+
if (!options.write)
|
|
97
|
+
return result;
|
|
98
|
+
// Every record is rewritten in memory first, so one without an id in its frontmatter
|
|
99
|
+
// refuses the run before anything is written. Only the frontmatter changes.
|
|
100
|
+
const rewritten = moves.map((move) => {
|
|
101
|
+
const end = move.source.indexOf("\n---", 4);
|
|
102
|
+
if (!move.source.startsWith("---\n") ||
|
|
103
|
+
end < 0 ||
|
|
104
|
+
!/^id:/m.test(move.source.slice(0, end)))
|
|
105
|
+
throw new Error(`${path.relative(oceanRoot(), move.file)} has no id in its frontmatter`);
|
|
106
|
+
let head = move.source
|
|
107
|
+
.slice(0, end)
|
|
108
|
+
.replace(/^id:.*$/m, `id: ${move.to}`);
|
|
109
|
+
if (!/^legacyId:/m.test(head))
|
|
110
|
+
head = head.replace(/^id:.*$/m, (line) => `${line}\nlegacyId: ${move.oldId}`);
|
|
111
|
+
return { move, source: head + move.source.slice(end) };
|
|
112
|
+
});
|
|
113
|
+
// The T counter first: once these tasks leave the shared namespace the scan no longer sees
|
|
114
|
+
// their numbers, and no project on T may reissue them.
|
|
115
|
+
const globalMax = await highestTaskNumber(LEGACY_PREFIX);
|
|
116
|
+
await writeManifest({
|
|
117
|
+
...ocean,
|
|
118
|
+
nextTask: Math.max(ocean.nextTask ?? 1, globalMax + 1),
|
|
119
|
+
});
|
|
120
|
+
// The prefix next, before any move: if a move fails, the ids already moved still resolve,
|
|
121
|
+
// and a rerun finishes the rest.
|
|
122
|
+
await writeManifest({ ...manifest, prefix, nextTask });
|
|
123
|
+
for (const { move, source } of rewritten) {
|
|
124
|
+
await atomicWrite(move.file, source);
|
|
125
|
+
if (move.to !== move.folder)
|
|
126
|
+
await rename(path.dirname(move.file), path.join(path.dirname(path.dirname(move.file)), move.to));
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function exists(target) {
|
|
132
|
+
return access(target).then(() => true, () => false);
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=renumber-tasks.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isWorkspaceProject, projectOfTasksRoot, repoRoot, resolveWithin, workspaceTasksRoot, } from "@ocean/core/shared/paths.js";
|
|
5
|
+
import { expandHome, projectVerify, readProjectManifest, } from "@ocean/core/shared/project-resolution.js";
|
|
6
|
+
import { declaredVerificationCommands } from "./verify-task.js";
|
|
7
|
+
// Whether `ocean tasks complete` will be able to verify this task, checked early and without
|
|
8
|
+
// running anything (T-291): T-283 named a test file that did not exist, which surfaced only at
|
|
9
|
+
// completion. Advisory: a checkpoint reports it and never refuses.
|
|
10
|
+
export async function verificationReadiness(id, root = workspaceTasksRoot()) {
|
|
11
|
+
const source = await readFile(resolveWithin(root, id, "task.md"), "utf8");
|
|
12
|
+
const { declared, refused } = declaredVerificationCommands(source);
|
|
13
|
+
const warnings = refused.map((command) => `declared verification is not allowed to run: ${command}`);
|
|
14
|
+
const project = projectOfTasksRoot(root);
|
|
15
|
+
const manifest = project
|
|
16
|
+
? await readProjectManifest(project).catch(() => null)
|
|
17
|
+
: null;
|
|
18
|
+
const runnable = declared.filter((command) => !refused.includes(command));
|
|
19
|
+
if (!declared.length && !(manifest && projectVerify(manifest).length))
|
|
20
|
+
warnings.push("no verification command declared and no project verify: tasks complete will refuse");
|
|
21
|
+
// Task commands run in the engine for the workspace project, else in the project's first path.
|
|
22
|
+
const cwd = project && !isWorkspaceProject(project) && manifest?.paths?.[0]
|
|
23
|
+
? expandHome(manifest.paths[0])
|
|
24
|
+
: repoRoot();
|
|
25
|
+
for (const command of runnable)
|
|
26
|
+
for (const file of missingTestFiles(command, cwd))
|
|
27
|
+
warnings.push(`test file not found: ${file} (in ${command})`);
|
|
28
|
+
return { ready: warnings.length === 0, warnings };
|
|
29
|
+
}
|
|
30
|
+
// Test files named after `node --test` that do not exist where the command will run. A
|
|
31
|
+
// `pnpm --filter <name> exec` prefix runs inside that workspace package.
|
|
32
|
+
function missingTestFiles(command, cwd) {
|
|
33
|
+
const missing = [];
|
|
34
|
+
for (const link of command.split(/\s*(?:&&|;)\s*/)) {
|
|
35
|
+
const tokens = link.trim().split(/\s+/);
|
|
36
|
+
const test = tokens.indexOf("--test");
|
|
37
|
+
if (test < 0 || !tokens.slice(0, test).includes("node"))
|
|
38
|
+
continue;
|
|
39
|
+
// `--filter <name>` or `--filter=<name>`.
|
|
40
|
+
const filter = tokens.findIndex((token) => token === "--filter" || token.startsWith("--filter="));
|
|
41
|
+
const name = filter < 0
|
|
42
|
+
? undefined
|
|
43
|
+
: tokens[filter].includes("=")
|
|
44
|
+
? tokens[filter].slice("--filter=".length)
|
|
45
|
+
: tokens[filter + 1];
|
|
46
|
+
const base = tokens[0] === "pnpm" && filter >= 0 ? packageDirectory(cwd, name) : cwd;
|
|
47
|
+
if (!base)
|
|
48
|
+
continue;
|
|
49
|
+
for (const arg of tokens.slice(test + 1))
|
|
50
|
+
if (!arg.startsWith("-") && /\.(?:[cm]?js|ts)$/.test(arg))
|
|
51
|
+
if (!existsSync(path.resolve(base, arg)))
|
|
52
|
+
missing.push(arg);
|
|
53
|
+
}
|
|
54
|
+
return missing;
|
|
55
|
+
}
|
|
56
|
+
function packageDirectory(cwd, name) {
|
|
57
|
+
if (!name)
|
|
58
|
+
return null;
|
|
59
|
+
const packages = path.join(cwd, "packages");
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = readdirSync(packages);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries)
|
|
68
|
+
try {
|
|
69
|
+
const manifest = JSON.parse(readFileSync(path.join(packages, entry, "package.json"), "utf8"));
|
|
70
|
+
if (manifest.name === name)
|
|
71
|
+
return path.join(packages, entry);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// not a package
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=verification-readiness.js.map
|