diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..722158a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Prepare smoke tests + run: | + bun link + continuum init + + - name: Validate + run: bun run validate diff --git a/.github/workflows/goal-guardrails.yml b/.github/workflows/goal-guardrails.yml deleted file mode 100644 index ba80868..0000000 --- a/.github/workflows/goal-guardrails.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: GOAL Guardrails - -on: - pull_request: - push: - -jobs: - validate-goal-invariants: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: bun.lock - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Link CLI - run: bun link - - - name: Typecheck - run: bun run typecheck - - - name: Run tests - run: bun run scripts/run-tests-for-validate.ts - - - name: Initialize continuum for smoke tests - run: | - continuum init - continuum memory init - - - name: Verify GOAL invariants - run: bun run verify:goal diff --git a/AGENTS.md b/AGENTS.md index bbdc032..13252af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,13 @@ ## Continuum -We use the `continuum` command (this repo is the source code) to keep track of our tasks and to serve as our tool for saving memory. +We use Continuum MCP tools (this repo is the source code) to keep track of tasks and save memory. Prefer the typed MCP tools through Executor; use the `continuum` CLI only as a fallback or for human operation. -At the beginning of a session run these commands. Use the guides as instruction on how to manage this repository: +At the beginning of a session: -- `continuum guide` -- explains usage and workflows and links to command specific guides -- `continuum init` +- Call `continuum_summary` for the current briefing. +- Call `continuum_init` only when the workspace is not initialized. +- Use `continuum guide` for CLI fallback documentation. Project documentation lives in `README.md`, `CONTRIBUTING.md`, and `LICENSE`. The sections below are agent-operational guidance not covered there. diff --git a/README.md b/README.md index 2aad81a..d60468e 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,23 @@ Global options must precede the command: Workspace resolution walks upward from the effective working directory to the nearest `.continuum` or `.git` directory. +### MCP + +`continuum mcp` serves the primary agent interface over local stdio MCP. It +provides typed tools for: + +- workspace initialization and summary +- task CRUD, validation, graph queries, steps, and notes +- memory append, search, and consolidation +- OpenCode recall status and import + +Each tool requires an absolute `workspace` path. Tool arguments are structured, +so multiline Markdown and shell-special characters are passed without shell +quoting. Configure an MCP client or gateway such as Executor to launch +`continuum mcp`. + +The CLI remains the human, scripting, and recovery interface. + ### Tasks `continuum task` provides persistent task planning and execution: diff --git a/bun.lock b/bun.lock index d128008..77dfb95 100644 --- a/bun.lock +++ b/bun.lock @@ -5,11 +5,12 @@ "": { "name": "continuum-memory-mvp", "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", "commander": "^14.0.3", "drizzle-orm": "^0.44.4", "effect": "^3.21.4", "yaml": "^2.8.1", - "zod": "^3.24.2", + "zod": "^3.25.0", }, "devDependencies": { "better-sqlite3": "^12.4.1", @@ -79,10 +80,20 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], @@ -91,82 +102,232 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "effect": ["effect@3.21.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-B89v/xSgPbl1J2Ai2u18jxq3odpFauU1rC6/eSs4FeNHi72kwKdJp12VGigvRV2lK+kRnx+OOz41XV8guZd4gQ=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hono": ["hono@4.12.29", "", {}, "sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], @@ -175,6 +336,8 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -183,26 +346,42 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tsx": ["tsx@4.22.3", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], diff --git a/package.json b/package.json index 2d78776..a12a001 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,12 @@ "verify:goal": "bun run scripts/verify-goal-invariants.ts" }, "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", "commander": "^14.0.3", "drizzle-orm": "^0.44.4", "effect": "^3.21.4", "yaml": "^2.8.1", - "zod": "^3.24.2" + "zod": "^3.25.0" }, "devDependencies": { "better-sqlite3": "^12.4.1", diff --git a/skills/continuum-task/SKILL.md b/skills/continuum-task/SKILL.md deleted file mode 100644 index 07ee139..0000000 --- a/skills/continuum-task/SKILL.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -name: continuum-task -description: Manage tasks via continuum task CLI -license: MIT -compatibility: opencode -metadata: - trigger: manual_user_request - priority: '900' - requires_state: 'writable' - min_version: '0.1.0' ---- - -# Continuum Task Skill - -## Purpose - -Provide a complete, command-by-command operational guide for the -`continuum task` CLI, including input conventions, safe workflows, -and examples for every subcommand. - -## Global Options - -Available on all commands: - -- `--json` Output JSON envelopes (`ok`, `data`, `meta`) -- `--cwd ` Run in a target directory -- `--quiet` Suppress non-JSON output - -## Input Conventions - -Text inputs can be provided directly, from files, or stdin: - -- `@-` read from stdin -- `@path/to/file` read from a file - -JSON inputs use the same convention: - -- `--input @file.json` for create -- `--patch @file.json` for update -- `--steps @file.json` for steps add - -For lists (like `--blocked-by`), values can be comma separated or repeated. - -## Valid Values - -Task status: - -- `open`, `ready`, `blocked`, `completed`, `cancelled`, `deleted` - -Task type: - -- `epic`, `feature`, `bug`, `investigation`, `chore` - -Task priority: - -- Integer (lower = higher priority) -- Default: `100` - -Step status: - -- `pending`, `in_progress`, `completed`, `skipped` - -Note kinds: - -- `discovery`, `decision` - -Note source: - -- `user`, `agent`, `system` - -## Command Reference - -### Initialize - -Create the local database in the current directory. - -```bash -continuum init -``` - -### List - -List tasks with filters and pagination. - -```bash -continuum task list --status ready --type feature --sort priority --order asc --limit 20 -``` - -Options: - -- `--status ` -- `--type ` -- `--parent ` -- `--include-deleted` -- `--cursor ` -- `--limit ` -- `--sort ` -- `--order ` - -Default ordering (when `--sort` is omitted): priority ascending, then createdAt ascending. - -### Get / View - -View a task, optionally expanding relations or printing a tree. - -```bash -continuum task get -continuum task view --expand all -continuum task get --tree -``` - -Options: - -- `--tree` show child tasks in a compact tree view -- `--expand ` comma-separated: `parent,children,blockers,all` -- `--include-deleted` include deleted children in tree/expand - -### Create - -Create a task using flags or JSON input. - -```bash -continuum task create --title "Add login audit" --type feature --description "@docs/task.md" -``` - -```bash -continuum task create --input @task.json -``` - -Supported fields: - -- `--title ` (required) -- `--type <type>` (required) -- `--status <status>` -- `--priority <number>` (lower = higher priority) -- `--intent <intent>` -- `--description <description>` -- `--plan <plan>` -- `--parent <task_id>` -- `--blocked-by <ids...>` - -### Update - -Update a task with direct flags or a JSON patch. - -```bash -continuum task update <task_id> --status ready --plan @plan.md -``` - -```bash -continuum task update <task_id> --patch @patch.json -``` - -Supported fields: - -- `--title <title>` -- `--type <type>` -- `--status <status>` -- `--priority <number>` (lower = higher priority) -- `--intent <intent>` -- `--description <description>` -- `--plan <plan>` -- `--parent <task_id>` -- `--blocked-by <ids...>` - -### Complete - -Complete a task with an outcome summary. - -```bash -continuum task complete <task_id> --outcome @outcome.md -``` - -### Delete - -Delete a task by id. Only do this when explicitly requested. - -```bash -continuum task delete <task_id> -``` - -### Validate - -Check whether a status transition is valid (missing fields, open blockers). - -```bash -continuum task validate <task_id> --transition completed -``` - -### Graph - -Query task relationships. - -```bash -continuum task graph ancestors <task_id> -continuum task graph descendants <task_id> -continuum task graph children <task_id> -``` - -### Templates - -List available task templates (types). - -```bash -continuum task templates list -``` - -### Steps - -Add, update, complete, or list task steps. - -Add steps (JSON array of TaskStepInput): - -```bash -continuum task steps add <task_id> --steps @steps.json -``` - -Update a step: - -```bash -continuum task steps update <task_id> <step_id> --status in_progress -``` - -Complete a step: - -```bash -continuum task steps complete <task_id> --step-id <step_id> --notes "Done" -``` - -Note: completing an already completed step returns a warning and makes no changes. - -List steps: - -```bash -continuum task steps list <task_id> -``` - -TaskStepInput shape: - -```json -[ - { - "title": "Investigate failure", - "description": "Reproduce issue and capture logs", - "status": "pending", - "position": 1, - "summary": null, - "notes": null - } -] -``` - -### Notes - -Add discoveries or decisions to a task. - -```bash -continuum task note add <task_id> --kind discovery --content "@notes.md" --source agent -``` - -Options: - -- `--kind <discovery|decision>` (required) -- `--content <content>` (required) -- `--rationale <rationale>` (decision support) -- `--impact <impact>` -- `--source <user|agent|system>` (default: agent) - -## Suggested Safe Workflow - -1. `init` once per repo -2. `task create` with intent/description/plan and an explicit priority -3. `task steps add` for executable subunits -4. `task note add` for discoveries/decisions -5. `task validate --transition ready|completed` before changing status -6. `task update --status ...` to progress -7. `task complete --outcome ...` when finished - -## JSON Output Contract - -When `--json` is provided, the CLI returns: - -```json -{ - "ok": true, - "data": {}, - "meta": { "cwd": "/path", "durationMs": 0 } -} -``` - -Errors follow: - -```json -{ - "ok": false, - "error": { "code": "...", "message": "..." }, - "meta": { "cwd": "/path", "durationMs": 0 } -} -``` diff --git a/skills/continuum/SKILL.md b/skills/continuum/SKILL.md new file mode 100644 index 0000000..3d783c5 --- /dev/null +++ b/skills/continuum/SKILL.md @@ -0,0 +1,64 @@ +--- +name: continuum +description: Use Continuum MCP for project tasks and durable memory +license: MIT +compatibility: opencode +metadata: + trigger: manual_user_request + priority: '900' + requires_state: 'writable' + min_version: '0.1.0' +--- + +# Continuum + +Continuum provides a local task ledger and durable project memory. Use its +typed MCP tools through Executor. Every call requires the target repository's +absolute `workspace` path. + +## Start + +Call `continuum_summary` at the beginning of a session for the current task and +memory briefing. Call `continuum_init` only when the workspace has not been +initialized. + +## Tasks + +Use `continuum_task_*`, `continuum_task_step_*`, and `continuum_task_note_add` +to track work that benefits from persistence across sessions. + +- Inspect existing tasks before creating one to avoid duplicates. +- Use lower priority numbers for more important work. +- Add executable steps when a task needs an explicit plan. +- Record durable discoveries and decisions as task notes. +- Validate a transition before marking a task ready or completed. +- Complete tasks with a concise outcome. +- Delete a task only when explicitly requested. + +Do not force every small request into a task or impose a fixed execution loop. +The user's current request and the repository's own instructions determine the +workflow. + +## Memory + +Use `continuum_memory_search` when prior decisions, failures, or implementation +context could affect the work. Use `continuum_memory_append` for context worth +retaining beyond the current session, not routine progress narration. + +Use `continuum_memory_consolidate` to turn pending journal entries into derived +memory. Journal entries and imported recall messages are immutable evidence; +consolidations and recall summaries are derived evidence. + +Recall is an explicit OpenCode import: + +- Use `continuum_recall_status` to inspect availability. +- Run `continuum_recall_import` with `dryRun: true` before importing when scope + or volume is uncertain. +- Import only when requested or when recovering relevant prior session context. + +Do not include secrets or credentials in tasks or memory. + +## Fallback + +The `continuum` CLI remains available for humans, scripting, recovery, and when +MCP is unavailable. Run `continuum guide` for its current command reference. diff --git a/skills/memory-manager/SKILL.md b/skills/memory-manager/SKILL.md deleted file mode 100644 index 8a525a8..0000000 --- a/skills/memory-manager/SKILL.md +++ /dev/null @@ -1,38 +0,0 @@ -# Continuum Memory Manager - -Continuum stores canonical memory in `.continuum/continuum.db`. Markdown files are generated, non-authoritative projections. - -## Workflow - -```bash -continuum summary -continuum memory search "<query>" -continuum memory append agent "<durable context>" -continuum memory consolidate -``` - -Append only information worth retaining. Journal entries and imported recall messages are immutable evidence; consolidations and recall summaries are derived evidence. - -## Recall - -```bash -continuum memory recall status -continuum memory recall import --dry-run -continuum memory recall import -continuum memory search "<query>" --source recall -``` - -Recall is an explicit OpenCode import, never a background synchronization lifecycle. - -## Migration - -Use `continuum memory migrate --dry-run` before importing historical NOW, RECENT, or MEMORY Markdown. Migration preserves source files and retains the legacy parsers needed to interpret them. - -## Core Commands - -- `memory append <user|agent|tool> <text...>` -- `memory consolidate [--dry-run]` -- `memory search <query...> [--source memory|recall|all] [--tier NOW|RECENT|MEMORY|all] [--tags ...] [--after ...] [--limit ...]` -- `memory recall status` -- `memory recall import` -- `memory migrate [--dry-run]` diff --git a/skills/task-loop/SKILL.md b/skills/task-loop/SKILL.md deleted file mode 100644 index 9ca3c0f..0000000 --- a/skills/task-loop/SKILL.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: task-loop -description: Run one task-loop iteration toward GOAL.md -license: MIT -compatibility: opencode -metadata: - trigger: manual_user_request - priority: '850' - requires_state: 'writable' - min_version: '0.1.0' ---- - -# Task Loop Skill - -## Purpose - -Run exactly one focused iteration toward `GOAL.md`, using `continuum task` as the execution ledger, then stop. - -## Entry Point (Re-focuser) - -`GOAL.md` at repo root is the north star. Read it at the start of every loop and produce a one-line alignment statement. -If a NOW memory file exists, read the latest entry to avoid duplicating work, but use task priority (lower number = higher priority) as the source of truth for what to resume; createdAt is only a tie-breaker. -If you resume from NOW, verify the task intent explicitly cites a GOAL.md section and update it before proceeding. -Treat `open` as the default working queue because `task create` defaults to `open`. -Priority ordering: lower numbers are higher priority (default is 100). - -## Preconditions - -- `GOAL.md` exists at repo root -- Continuum task DB initialized (`continuum init`) - -If either is missing, stop and ask the user to fix it. - -## Single-Iteration Loop (Hard Stop After One Step) - -1. Read `GOAL.md` and write a one-line alignment statement. -2. Search memory for prior context on this goal area: - ```bash - continuum memory search "<GOAL keyword or active task title>" - ``` - Run from the target repo (or pass `--cwd <repo-or-subdir>`) so the search - targets that workspace's local `.continuum` state. This command searches - materialized memory and recall summaries together by default. - Note any prior task IDs, known failures, or decisions for this area. Proceed regardless of results. -3. Preflight to avoid redundant work: - - List `open` tasks. - - If the list is empty, create one `investigation` task focused on discovering GOAL-aligned work in the current repo. This task should explicitly state the exploration criteria and expected outputs (new concrete tasks with priorities and evidence). - - For the top priority open task, quickly check whether existing code/tests/docs already implement it. - - If already implemented, add a discovery note and skip to the next task. - - If all open tasks appear already implemented, complete them with notes and stop. -4. Select a task (avoid creating duplicates; prefer `open`, ordered by priority): - - If a `task_id` was provided, use it. - - Else list `open` tasks sorted by priority (lowest number first; createdAt only breaks ties) and select the top task. - - If the latest NOW task is still `open` or `ready` and has a priority number less than or equal to the selected task, resume it. - - Else choose the highest-priority `ready` task (only if your workflow explicitly uses `ready`). - - If no `open` or `ready` tasks exist, create one `investigation` task from `GOAL.md` and assign it a high priority (lower number than build work for the same focus). - - If an `investigation` task exists for the same focus, ensure it has a lower priority number than build tasks and select it first. - - Never create a new task if an existing `open`/`ready` task already has the same intent/title. -5. Ensure task intent references a section of `GOAL.md` and add a minimal plan if missing (update the task before validation). -6. Pick the first `pending` step. - - If no steps exist, add 1-3 concrete steps derived from GOAL.md success criteria (avoid "run the loop" phrasing). -7. Append a step-start memory entry (run before beginning work): - ```bash - continuum memory append agent "Task: <task_id>; Step: <step_id>; Status: starting; Intent: <one-line step goal>" - ``` -8. Mark the step `in_progress`. -9. Do the work (code, docs, config) that completes the step. - - If the selected task is `investigation`, perform a focused repo exploration and create 1-5 concrete follow-up tasks directly in `continuum task`. - - Every generated task must include GOAL alignment, a concise plan, and evidence (code/test/doc reference) so it is actionable. - - De-duplicate against existing `open`/`ready`/`in_progress`/`completed` tasks before creating anything new. -10. Validate (one command at a time, they don't `&&` well): - - Run `bun run typecheck` - - Run `bun test` - - Run one smoke command relevant to the work (example: `continuum task list --json`) - -11. Add notes (discovery/decision) to the task as needed. -12. Complete the step with a concise summary. -13. Flush task notes to NOW memory: - ```bash - continuum task notes flush <task_id> - ``` - Skip this step if Phase 2 has not shipped yet (`task notes flush` command does not exist). -14. If all steps are complete, validate and complete the task. -15. Append the full loop-end memory entry (auto-starts a session if missing): - ```bash - continuum memory append agent "..." - ``` -16. Stop and return control to the user. - -## Commands (Reference) - -Create an investigation task when queue is empty: - -```bash -continuum task create --title "Discover next GOAL-aligned work" --type investigation --priority 50 --intent "Aligned to GOAL.md: <section>; investigate repo gaps and generate actionable tasks" --description "Explore code/tests/docs for missing or stale work aligned to GOAL.md success criteria. Create 1-5 deduplicated tasks with evidence and priority." --plan "Plan: 1) Inspect GOAL criteria vs current implementation 2) Identify highest-value gaps with evidence 3) Create prioritized follow-up tasks" -``` - -List open tasks (default queue, priority-ordered): - -```bash -continuum task list --status open --sort priority --order asc --limit 20 -``` - -List ready tasks (optional queue): - -```bash -continuum task list --status ready --sort priority --order asc --limit 1 -``` - -Get task details: - -```bash -continuum task get <task_id> -``` - -Create task from goal (leave status as default unless you explicitly use a ready queue): - -```bash -continuum task create --title "<title from GOAL.md success criteria>" --type feature --priority 100 --intent "Aligned to GOAL.md: <section>" --description "@GOAL.md" --plan "Plan: <1-3 steps>" -``` - -List steps: - -```bash -continuum task steps list <task_id> -``` - -Add steps: - -```bash -continuum task steps add <task_id> --steps @steps.json -``` - -Add or update plan: - -```bash -continuum task update <task_id> --plan "Plan: <1-3 steps>" -``` - -Mark in progress: - -```bash -continuum task steps update <task_id> <step_id> --status in_progress -``` - -Complete step: - -```bash -continuum task steps complete <task_id> --step-id <step_id> --notes "Done" -``` - -Add note: - -```bash -continuum task note add <task_id> --kind discovery --content "@notes.md" --source agent -``` - -Validate and complete task: - -```bash -continuum task validate <task_id> --transition completed -continuum task complete <task_id> --outcome @outcome.md -``` - -## NOW Memory Entry (Minimum) - -Append a short entry via CLI (auto-starts a NOW session if missing): -Run a shorter version (Task, Step, Status, Intent only) at step 7 (start); run the full version at step 15 (end). - -```bash -continuum memory append agent "Goal alignment: ...; Task: ...; Step: ...; Changes: ...; Tests: ...; Outcome: ..." -``` - -Include: - -- Goal alignment: one-line summary -- Task: `<task_id>` -- Step: `<step_id>` -- Summary: - <summary_of_work_markdown_blob> -- Discoveries: - <discoveries_markdown_blob> -- Changes: file paths touched -- Tests: commands run and results -- Outcome: one sentence - -Do not include secrets or credentials. - -## Stop Conditions - -- One step completed, OR -- Validation failed (log failure and stop), OR -- Blocked by missing info (log and stop) diff --git a/src/cli.ts b/src/cli.ts index 8bf4a4b..338ea76 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,6 +4,7 @@ import { createTaskCommand } from './cli/commands/task' import { createSetupCommand } from './cli/commands/setup' import { createGuideCommand } from './cli/commands/guide' import { createSummaryCommand } from './cli/commands/summary' +import { createMcpCommand } from './cli/commands/mcp' import { runCommand } from './cli/io' import continuum from './sdk' import { @@ -59,6 +60,7 @@ function createProgram(): Command { program.addCommand(createSetupCommand()) program.addCommand(createGuideCommand()) program.addCommand(createSummaryCommand()) + program.addCommand(createMcpCommand()) program.addCommand(createMemoryCommand()) program.addCommand(createTaskCommand()) program.exitOverride() diff --git a/src/cli/commands/guide.ts b/src/cli/commands/guide.ts index 8616bc4..bb4e15a 100644 --- a/src/cli/commands/guide.ts +++ b/src/cli/commands/guide.ts @@ -35,6 +35,9 @@ const overviewGuide = `# Continuum Agent Guide Use Continuum for durable task state and project memory. +Agents should prefer the typed Continuum MCP tools through Executor. The CLI +commands below are equivalent fallback and human-operated workflows. + Start of session: 1. continuum summary 2. continuum task list --status ready --sort priority --order asc diff --git a/src/cli/commands/mcp.ts b/src/cli/commands/mcp.ts new file mode 100644 index 0000000..1b3b090 --- /dev/null +++ b/src/cli/commands/mcp.ts @@ -0,0 +1,8 @@ +import { Command } from 'commander' +import { serveContinuumMcp } from '../../mcp/server' + +export function createMcpCommand(): Command { + return new Command('mcp') + .description('Serve Continuum tools over MCP using stdio') + .action(serveContinuumMcp) +} diff --git a/src/cli/commands/memory/recall-basic-handlers.ts b/src/cli/commands/memory/recall-basic-handlers.ts index 155a924..2778390 100644 --- a/src/cli/commands/memory/recall-basic-handlers.ts +++ b/src/cli/commands/memory/recall-basic-handlers.ts @@ -6,20 +6,14 @@ import { Effect } from 'effect' import { runMemoryCommand } from '../../io' import { MemoryRuntime } from '../../../memory/runtime/memory-runtime' import { makeRecallRepository } from '../../../memory/repository/recall-repository' +import { getRecallStatus } from '../../../memory/application/recall-status' export async function handleRecallStatus(command: Command): Promise<void> { await runMemoryCommand( command, Effect.gen(function* () { const runtime = yield* MemoryRuntime - return runtime.handle.sqlite - .query( - `SELECT - (SELECT COUNT(*) FROM memory_recall_sources) sources, - (SELECT COUNT(*) FROM memory_recall_messages) messages, - (SELECT COUNT(*) FROM memory_recall_summaries) summaries`, - ) - .get() as { sources: number; messages: number; summaries: number } + return getRecallStatus(runtime.handle) }), (counts) => { console.log('Canonical recall status:') diff --git a/src/cli/commands/summary-tasks.ts b/src/cli/commands/summary-tasks.ts index e32fe8e..5c2b292 100644 --- a/src/cli/commands/summary-tasks.ts +++ b/src/cli/commands/summary-tasks.ts @@ -1,5 +1,7 @@ -import continuum, { isContinuumError } from '../../sdk' +import { isContinuumError } from '../../sdk' +import { map_task } from '../../sdk/mappers' import type { Task, TaskNote } from '../../sdk/types' +import { list_tasks_for_directory } from '../../task/tasks.service' export type TaskSummary = { initialized: boolean @@ -12,22 +14,25 @@ export type TaskSummary = { nextTask: Task | null } -export async function loadTaskSummary(limit: number): Promise<TaskSummary> { +export async function loadTaskSummary( + limit: number, + directory: string = process.cwd(), +): Promise<TaskSummary> { try { const [result, completedResult] = await Promise.all([ - continuum.task.list({ + list_tasks_for_directory(directory, { limit: 1000, sort: 'priority', order: 'asc', }), - continuum.task.list({ + list_tasks_for_directory(directory, { status: 'completed', limit, sort: 'updatedAt', order: 'desc', }), ]) - const active = result.tasks + const active = result.tasks.map(map_task) const ready = active.filter((task) => task.status === 'ready') const open = active.filter((task) => task.status === 'open') const blocked = active.filter((task) => task.status === 'blocked') @@ -37,7 +42,7 @@ export async function loadTaskSummary(limit: number): Promise<TaskSummary> { ready, open, blocked, - completed: completedResult.tasks, + completed: completedResult.tasks.map(map_task), nextTask: ready[0] ?? open[0] ?? blocked[0] ?? null, } } catch (error) { diff --git a/src/mcp/init-tool.ts b/src/mcp/init-tool.ts new file mode 100644 index 0000000..0755237 --- /dev/null +++ b/src/mcp/init-tool.ts @@ -0,0 +1,23 @@ +import { getDbClientByPath } from '../db/client' +import { dbFilePath } from '../db/paths' +import { init_project, init_status } from '../task/util' +import { resolveInitWorkspace } from './tools' + +export async function initMcpWorkspace(input: { workspace: string }): Promise<{ + workspace: string + dbPath: string + created: boolean + initialized: boolean +}> { + const workspace = resolveInitWorkspace(input.workspace) + const before = await init_status({ directory: workspace }) + await init_project({ directory: workspace }) + getDbClientByPath(dbFilePath(workspace)) + const after = await init_status({ directory: workspace }) + return { + workspace, + dbPath: dbFilePath(workspace), + created: !before.dbFileExists, + initialized: after.pluginDirExists && after.dbFileExists, + } +} diff --git a/src/mcp/memory-tools.ts b/src/mcp/memory-tools.ts new file mode 100644 index 0000000..1496183 --- /dev/null +++ b/src/mcp/memory-tools.ts @@ -0,0 +1,102 @@ +import { isAbsolute } from 'node:path' +import { getDbClientByPath } from '../db/client' +import { consolidateMemory } from '../memory/application/consolidate' +import { importCanonicalOpencodeRecall } from '../memory/application/recall-import' +import { getRecallStatus } from '../memory/application/recall-status' +import { getMemoryConfig } from '../memory/config' +import { makeConsolidationRepository } from '../memory/repository/consolidation-repository' +import { makeJournalRepository } from '../memory/repository/journal-repository' +import { makeRecallRepository } from '../memory/repository/recall-repository' +import { resolveMcpWorkspace } from './tools' +import { runMcpEffect } from './result' + +export async function consolidateMcpMemory(input: { + workspace: string + dryRun?: boolean +}): Promise<Record<string, unknown>> { + const context = resolveMcpWorkspace(input.workspace) + const handle = getDbClientByPath(context.dbPath) + const result = await runMcpEffect( + consolidateMemory({ + dbPath: context.dbPath, + memoryDir: context.memoryDir, + dryRun: input.dryRun, + config: getMemoryConfig(context.memoryDir), + journal: makeJournalRepository(handle), + consolidations: makeConsolidationRepository(handle), + }), + ) + if (result.status === 'conflict') { + return { + workspace: context.workspaceRoot, + status: result.status, + dryRun: result.dryRun, + expectedBoundary: result.error.expectedBoundary, + actualBoundary: result.error.actualBoundary, + } + } + if (result.status === 'completed') { + return { + workspace: context.workspaceRoot, + status: result.status, + dryRun: result.dryRun, + consolidation: result.consolidation, + entryCount: result.entryCount, + projectionStale: result.projection.stale, + } + } + return { workspace: context.workspaceRoot, ...result } +} + +export function getMcpRecallStatus(input: { workspace: string }): { + workspace: string + sources: number + rawMessages: number + derivedSummaries: number +} { + const context = resolveMcpWorkspace(input.workspace) + const status = getRecallStatus(getDbClientByPath(context.dbPath)) + return { + workspace: context.workspaceRoot, + sources: status.sources, + rawMessages: status.messages, + derivedSummaries: status.summaries, + } +} + +export async function importMcpRecall(input: { + workspace: string + sourceDb?: string + projectId?: string + sessionId?: string + after?: string + limit?: number + dryRun?: boolean +}): Promise<Record<string, unknown>> { + const context = resolveMcpWorkspace(input.workspace) + if (input.sourceDb && !isAbsolute(input.sourceDb)) { + throw new Error('sourceDb must be an absolute path') + } + const afterDate = input.after ? parseDate(input.after) : undefined + const handle = getDbClientByPath(context.dbPath) + const result = await runMcpEffect( + importCanonicalOpencodeRecall({ + continuumDbPath: context.dbPath, + dbPath: input.sourceDb, + repoPath: context.workspaceRoot, + projectId: input.projectId, + sessionId: input.sessionId, + afterDate, + limit: input.limit, + dryRun: input.dryRun, + repository: makeRecallRepository(handle), + }), + ) + return { workspace: context.workspaceRoot, ...result } +} + +function parseDate(value: string): Date { + const date = new Date(value) + if (Number.isNaN(date.getTime())) throw new Error(`Invalid date: ${value}`) + return date +} diff --git a/src/mcp/register-memory-tools.ts b/src/mcp/register-memory-tools.ts new file mode 100644 index 0000000..c0e25f0 --- /dev/null +++ b/src/mcp/register-memory-tools.ts @@ -0,0 +1,67 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'zod' +import { + consolidateMcpMemory, + getMcpRecallStatus, + importMcpRecall, +} from './memory-tools' +import { toolResult } from './result' +import { positiveIntegerSchema, workspaceSchema } from './task-schemas' + +export function registerMemoryTools(server: McpServer): void { + server.registerTool( + 'continuum_memory_consolidate', + { + description: + 'Consolidate pending immutable journal entries; dryRun may still call the configured summarizer.', + inputSchema: { + workspace: workspaceSchema(), + dryRun: z.boolean().optional(), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (input) => toolResult(await consolidateMcpMemory(input)), + ) + server.registerTool( + 'continuum_recall_status', + { + description: 'Read canonical OpenCode recall inventory counts.', + inputSchema: { workspace: workspaceSchema() }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async (input) => toolResult(getMcpRecallStatus(input)), + ) + server.registerTool( + 'continuum_recall_import', + { + description: + 'Import OpenCode sessions into canonical recall; dryRun performs no writes or LLM calls.', + inputSchema: { + workspace: workspaceSchema(), + sourceDb: z.string().optional(), + projectId: z.string().optional(), + sessionId: z.string().optional(), + after: z.string().optional(), + limit: positiveIntegerSchema().max(1000).optional(), + dryRun: z.boolean().optional(), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (input) => toolResult(await importMcpRecall(input)), + ) +} diff --git a/src/mcp/register-task-tools.ts b/src/mcp/register-task-tools.ts new file mode 100644 index 0000000..9f990db --- /dev/null +++ b/src/mcp/register-task-tools.ts @@ -0,0 +1,250 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'zod' +import { + addMcpTaskNote, + addMcpTaskSteps, + completeMcpTask, + completeMcpTaskStep, + createMcpTask, + deleteMcpTask, + getMcpTask, + graphMcpTasks, + listMcpTasks, + listMcpTaskSteps, + updateMcpTask, + updateMcpTaskStep, + validateMcpTask, +} from './task-tools' +import { initMcpWorkspace } from './init-tool' +import { + idSchema, + listTaskStatusSchema, + positiveIntegerSchema, + taskCreateSchema, + taskPatchSchema, + taskStatusSchema, + taskStepPatchSchema, + taskStepSchema, + taskTypeSchema, + workspaceSchema, +} from './task-schemas' +import { toolResult } from './result' + +const readOnly = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +} +const write = { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, +} + +export function registerTaskTools(server: McpServer): void { + registerTaskReadTools(server) + registerTaskWriteTools(server) + registerTaskStepTools(server) + registerTaskNoteTool(server) + registerInitTool(server) +} + +function registerTaskReadTools(server: McpServer): void { + server.registerTool( + 'continuum_task_list', + { + description: 'List project tasks with filtering and cursor pagination.', + inputSchema: { + workspace: workspaceSchema(), + options: z + .object({ + status: listTaskStatusSchema().optional(), + type: taskTypeSchema().optional(), + parentId: z.string().nullable().optional(), + includeDeleted: z.boolean().optional(), + cursor: z.string().optional(), + limit: positiveIntegerSchema().max(1000).optional(), + sort: z.enum(['createdAt', 'updatedAt', 'priority']).optional(), + order: z.enum(['asc', 'desc']).optional(), + }) + .optional(), + }, + annotations: readOnly, + }, + async (input) => toolResult(await listMcpTasks(input)), + ) + server.registerTool( + 'continuum_task_get', + { + description: 'Get one task with optional relationship expansion.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + expand: z.array(z.enum(['parent', 'children', 'blockers'])).optional(), + includeDeleted: z.boolean().optional(), + }, + annotations: readOnly, + }, + async (input) => toolResult(await getMcpTask(input)), + ) + server.registerTool( + 'continuum_task_validate', + { + description: 'Validate whether a task is ready for a target status.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + transition: taskStatusSchema(), + }, + annotations: readOnly, + }, + async (input) => toolResult(await validateMcpTask(input)), + ) + server.registerTool( + 'continuum_task_graph', + { + description: 'Query task ancestors, descendants, or direct children.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + query: z.enum(['ancestors', 'descendants', 'children']), + }, + annotations: readOnly, + }, + async (input) => toolResult(await graphMcpTasks(input)), + ) +} + +function registerTaskWriteTools(server: McpServer): void { + server.registerTool( + 'continuum_task_create', + { + description: 'Create a task in an initialized workspace.', + inputSchema: { workspace: workspaceSchema(), task: taskCreateSchema() }, + annotations: write, + }, + async (input) => toolResult(await createMcpTask(input)), + ) + server.registerTool( + 'continuum_task_update', + { + description: 'Update task fields.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + patch: taskPatchSchema(), + }, + annotations: write, + }, + async (input) => toolResult(await updateMcpTask(input)), + ) + server.registerTool( + 'continuum_task_complete', + { + description: 'Complete a task with an outcome summary.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + outcome: z.string().min(1), + }, + annotations: write, + }, + async (input) => toolResult(await completeMcpTask(input)), + ) + server.registerTool( + 'continuum_task_delete', + { + description: 'Soft-delete a task.', + inputSchema: { workspace: workspaceSchema(), id: idSchema() }, + annotations: { ...write, destructiveHint: true, idempotentHint: true }, + }, + async (input) => toolResult(await deleteMcpTask(input)), + ) +} + +function registerTaskStepTools(server: McpServer): void { + server.registerTool( + 'continuum_task_steps_list', + { + description: 'List the steps on a task.', + inputSchema: { workspace: workspaceSchema(), id: idSchema() }, + annotations: readOnly, + }, + async (input) => toolResult(await listMcpTaskSteps(input)), + ) + server.registerTool( + 'continuum_task_steps_add', + { + description: 'Add one or more ordered steps to a task.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + steps: z.array(taskStepSchema()).min(1), + }, + annotations: write, + }, + async (input) => toolResult(await addMcpTaskSteps(input)), + ) + server.registerTool( + 'continuum_task_steps_update', + { + description: 'Update one task step.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + stepId: idSchema(), + patch: taskStepPatchSchema(), + }, + annotations: write, + }, + async (input) => toolResult(await updateMcpTaskStep(input)), + ) + server.registerTool( + 'continuum_task_steps_complete', + { + description: 'Complete a task step, defaulting to the current step.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + stepId: idSchema().optional(), + notes: z.string().optional(), + }, + annotations: write, + }, + async (input) => toolResult(await completeMcpTaskStep(input)), + ) +} + +function registerTaskNoteTool(server: McpServer): void { + server.registerTool( + 'continuum_task_note_add', + { + description: 'Add a discovery or decision note to a task.', + inputSchema: { + workspace: workspaceSchema(), + id: idSchema(), + kind: z.enum(['discovery', 'decision']), + content: z.string().min(1), + source: z.enum(['user', 'agent', 'system']).optional(), + rationale: z.string().optional(), + impact: z.string().optional(), + }, + annotations: write, + }, + async (input) => toolResult(await addMcpTaskNote(input)), + ) +} + +function registerInitTool(server: McpServer): void { + server.registerTool( + 'continuum_init', + { + description: 'Initialize and migrate Continuum in an existing directory.', + inputSchema: { workspace: workspaceSchema() }, + annotations: { ...write, idempotentHint: true }, + }, + async (input) => toolResult(await initMcpWorkspace(input)), + ) +} diff --git a/src/mcp/result.ts b/src/mcp/result.ts new file mode 100644 index 0000000..2ff98cd --- /dev/null +++ b/src/mcp/result.ts @@ -0,0 +1,29 @@ +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' +import { Effect } from 'effect' + +export function toolResult<T extends Record<string, unknown>>( + result: T, + render: (result: T) => string = (value) => JSON.stringify(value, null, 2), +): CallToolResult { + return { + content: [{ type: 'text' as const, text: render(result) }], + structuredContent: result, + } +} + +export async function runMcpEffect<A, E>( + effect: Effect.Effect<A, E>, +): Promise<A> { + return Effect.runPromise(effect.pipe(Effect.mapError(mcpError))) +} + +function mcpError(cause: unknown): Error { + if (cause instanceof Error && cause.message) return cause + if (cause && typeof cause === 'object' && 'cause' in cause) { + return mcpError(cause.cause) + } + if (cause && typeof cause === 'object' && '_tag' in cause) { + return new Error(String(cause._tag)) + } + return new Error(String(cause)) +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 0000000..59e54c8 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,98 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { z } from 'zod' +import { appendMcpMemory, getSummary, searchMcpMemory } from './tools' +import { registerTaskTools } from './register-task-tools' +import { registerMemoryTools } from './register-memory-tools' +import { toolResult } from './result' + +const workspaceSchema = () => + z + .string() + .min(1) + .describe('Absolute path inside the initialized Continuum workspace') +const positiveIntegerSchema = () => z.number().int().positive() + +export function createContinuumMcpServer(): McpServer { + const server = new McpServer({ name: 'continuum', version: '0.1.1' }) + + registerTaskTools(server) + registerMemoryTools(server) + + server.registerTool( + 'continuum_summary', + { + description: 'Read the current task and memory briefing for a workspace.', + inputSchema: { + workspace: workspaceSchema(), + taskLimit: positiveIntegerSchema().optional(), + memoryLimit: positiveIntegerSchema().optional(), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async (input) => + toolResult(await getSummary(input), (result) => result.output), + ) + + server.registerTool( + 'continuum_memory_append', + { + description: + 'Append one immutable, fully formatted entry to workspace memory.', + inputSchema: { + workspace: workspaceSchema(), + kind: z.enum(['user', 'agent', 'tool']), + content: z + .string() + .min(1) + .describe('Full entry text; Markdown is allowed'), + tags: z.array(z.string().min(1)).optional(), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + async (input) => + toolResult( + await appendMcpMemory(input), + (result) => + `Appended memory entry ${result.id} at sequence ${result.sequence}.`, + ), + ) + + server.registerTool( + 'continuum_memory_search', + { + description: 'Search canonical memory in an initialized workspace.', + inputSchema: { + workspace: workspaceSchema(), + query: z.string().min(1), + limit: positiveIntegerSchema().max(100).optional(), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async (input) => + toolResult(await searchMcpMemory(input), (result) => + JSON.stringify(result.matches, null, 2), + ), + ) + + return server +} + +export async function serveContinuumMcp(): Promise<void> { + await createContinuumMcpServer().connect(new StdioServerTransport()) +} diff --git a/src/mcp/task-schemas.ts b/src/mcp/task-schemas.ts new file mode 100644 index 0000000..4a72074 --- /dev/null +++ b/src/mcp/task-schemas.ts @@ -0,0 +1,91 @@ +import { z } from 'zod' +import type { + CreateTaskInput, + TaskStatus, + TaskStep, + TaskStepInput, + TaskType, + UpdateTaskInput, +} from '../sdk/types' + +export function workspaceSchema(): z.ZodString { + return z + .string() + .min(1) + .describe('Absolute path inside the Continuum workspace') +} +export function idSchema(): z.ZodString { + return z.string().min(1) +} +export function positiveIntegerSchema(): z.ZodNumber { + return z.number().int().positive() +} +export function taskTypeSchema(): z.ZodType<TaskType> { + return z.enum(['epic', 'feature', 'bug', 'investigation', 'chore']) +} +export function taskStatusSchema(): z.ZodType<Exclude<TaskStatus, 'deleted'>> { + return z.enum(['open', 'ready', 'blocked', 'completed', 'cancelled']) +} +export function listTaskStatusSchema(): z.ZodType<TaskStatus> { + return z.enum([ + 'open', + 'ready', + 'blocked', + 'completed', + 'cancelled', + 'deleted', + ]) +} +export function stepStatusSchema(): z.ZodType<TaskStep['status']> { + return z.enum(['pending', 'in_progress', 'completed', 'skipped']) +} + +export function taskCreateSchema(): z.ZodType<CreateTaskInput> { + return z.object({ + title: z.string().min(1), + type: taskTypeSchema(), + status: taskStatusSchema().optional(), + priority: z.number().int().nullable().optional(), + intent: z.string().nullable().optional(), + description: z.string(), + plan: z.string().nullable().optional(), + parentId: z.string().nullable().optional(), + blockedBy: z.array(z.string()).nullable().optional(), + }) +} + +export function taskPatchSchema(): z.ZodType<UpdateTaskInput> { + return z.object({ + title: z.string().min(1).optional(), + type: taskTypeSchema().optional(), + status: taskStatusSchema().optional(), + priority: z.number().int().nullable().optional(), + intent: z.string().nullable().optional(), + description: z.string().optional(), + plan: z.string().nullable().optional(), + parentId: z.string().nullable().optional(), + blockedBy: z.array(z.string()).nullable().optional(), + }) +} + +export function taskStepSchema(): z.ZodType<TaskStepInput> { + return z.object({ + title: z.string().min(1), + description: z.string().min(1), + status: stepStatusSchema().optional(), + position: z.number().int().nullable().optional(), + summary: z.string().nullable().optional(), + notes: z.string().nullable().optional(), + }) +} + +export function taskStepPatchSchema(): z.ZodType<Partial<TaskStep>> { + return z.object({ + title: z.string().min(1).optional(), + description: z.string().optional(), + status: stepStatusSchema().optional(), + position: z.number().int().nullable().optional(), + summary: z.string().nullable().optional(), + notes: z.string().nullable().optional(), + }) +} diff --git a/src/mcp/task-tool-util.ts b/src/mcp/task-tool-util.ts new file mode 100644 index 0000000..dde8258 --- /dev/null +++ b/src/mcp/task-tool-util.ts @@ -0,0 +1,28 @@ +import { map_task } from '../sdk/mappers' +import type { Task } from '../sdk/types' +import { ContinuumError } from '../task/error' +import { get_task_for_directory } from '../task/tasks.service' + +export async function requireTask( + directory: string, + id: string, +): Promise<Task> { + const task = await getMappedTask(directory, id) + if (!task) throw new ContinuumError('TASK_NOT_FOUND', 'Task not found') + return task +} + +export async function getMappedTask( + directory: string, + id: string, +): Promise<Task | null> { + const task = await get_task_for_directory(directory, id) + return task ? map_task(task) : null +} + +export function parseStepId(value: string): number { + const parsed = Number(value) + if (!Number.isInteger(parsed) || parsed <= 0) + throw new ContinuumError('ITEM_NOT_FOUND', 'Invalid step id') + return parsed +} diff --git a/src/mcp/task-tools.ts b/src/mcp/task-tools.ts new file mode 100644 index 0000000..8611ed2 --- /dev/null +++ b/src/mcp/task-tools.ts @@ -0,0 +1,287 @@ +import { map_create_input, map_task, map_update_input } from '../sdk/mappers' +import { query_task_graph } from '../sdk/graph' +import type { + CreateTaskInput, + ListTasksOptions, + Task, + TaskGraphQuery, + TaskStatus, + TaskStep, + TaskStepInput, + UpdateTaskInput, +} from '../sdk/types' +import { ContinuumError } from '../task/error' +import { + add_decision_for_directory, + add_discovery_for_directory, + add_steps_for_directory, + complete_step_for_directory, + complete_task_for_directory, + create_task_for_directory, + delete_task_for_directory, + get_open_blockers_for_directory, + get_task_for_directory, + list_tasks_for_directory, + update_step_for_directory, + update_task_for_directory, +} from '../task/tasks.service' +import { validate_status_transition } from '../task/validation' +import { getMappedTask, parseStepId, requireTask } from './task-tool-util' +import { resolveMcpWorkspace } from './tools' + +export async function listMcpTasks(input: { + workspace: string + options?: ListTasksOptions +}): Promise<{ workspace: string; tasks: Task[]; nextCursor?: string }> { + const context = resolveMcpWorkspace(input.workspace) + const options = input.options ?? {} + const result = await list_tasks_for_directory(context.workspaceRoot, { + status: options.status, + type: options.type, + parent_id: options.parentId, + includeDeleted: options.includeDeleted, + cursor: options.cursor, + limit: options.limit, + sort: options.sort, + order: options.order, + }) + return { + workspace: context.workspaceRoot, + tasks: result.tasks.map(map_task), + nextCursor: result.nextCursor, + } +} + +export async function getMcpTask(input: { + workspace: string + id: string + expand?: Array<'parent' | 'children' | 'blockers'> + includeDeleted?: boolean +}): Promise<{ + workspace: string + task: Task + parent?: Task | null + children?: Task[] + blockers?: Task[] +}> { + const context = resolveMcpWorkspace(input.workspace) + const task = await requireTask(context.workspaceRoot, input.id) + const expand = new Set(input.expand ?? []) + const parent = + expand.has('parent') && task.parentId + ? await getMappedTask(context.workspaceRoot, task.parentId) + : undefined + const children = expand.has('children') + ? ( + await listMcpTasks({ + workspace: context.workspaceRoot, + options: { + parentId: task.id, + includeDeleted: input.includeDeleted, + limit: 1000, + }, + }) + ).tasks + : undefined + const blockers = expand.has('blockers') + ? ( + await Promise.all( + task.blockedBy.map((id) => getMappedTask(context.workspaceRoot, id)), + ) + ).filter((item): item is Task => item !== null) + : undefined + return { workspace: context.workspaceRoot, task, parent, children, blockers } +} + +export async function createMcpTask(input: { + workspace: string + task: CreateTaskInput +}): Promise<{ workspace: string; task: Task }> { + const context = resolveMcpWorkspace(input.workspace) + const task = await create_task_for_directory( + context.workspaceRoot, + map_create_input(input.task), + ) + return { workspace: context.workspaceRoot, task: map_task(task) } +} + +export async function updateMcpTask(input: { + workspace: string + id: string + patch: UpdateTaskInput +}): Promise<{ workspace: string; task: Task }> { + const context = resolveMcpWorkspace(input.workspace) + const task = await update_task_for_directory( + context.workspaceRoot, + input.id, + map_update_input(input.patch), + ) + return { workspace: context.workspaceRoot, task: map_task(task) } +} + +export async function completeMcpTask(input: { + workspace: string + id: string + outcome: string +}): Promise<{ workspace: string; task: Task }> { + const context = resolveMcpWorkspace(input.workspace) + const task = await complete_task_for_directory(context.workspaceRoot, { + task_id: input.id, + outcome: input.outcome, + }) + return { workspace: context.workspaceRoot, task: map_task(task) } +} + +export async function deleteMcpTask(input: { + workspace: string + id: string +}): Promise<{ workspace: string; deleted: true; id: string }> { + const context = resolveMcpWorkspace(input.workspace) + await delete_task_for_directory(context.workspaceRoot, input.id) + return { workspace: context.workspaceRoot, deleted: true, id: input.id } +} + +export async function validateMcpTask(input: { + workspace: string + id: string + transition: Exclude<TaskStatus, 'deleted'> +}): Promise<{ + workspace: string + id: string + transition: Exclude<TaskStatus, 'deleted'> + valid: boolean + missingFields: string[] + openBlockers: string[] +}> { + const context = resolveMcpWorkspace(input.workspace) + const raw = await get_task_for_directory(context.workspaceRoot, input.id) + if (!raw) throw new ContinuumError('TASK_NOT_FOUND', 'Task not found') + const missingFields = validate_status_transition(raw, input.transition) + const openBlockers = + input.transition === 'completed' + ? await get_open_blockers_for_directory(context.workspaceRoot, input.id) + : [] + return { + workspace: context.workspaceRoot, + id: input.id, + transition: input.transition, + valid: missingFields.length === 0 && openBlockers.length === 0, + missingFields, + openBlockers, + } +} + +export async function graphMcpTasks(input: { + workspace: string + id: string + query: TaskGraphQuery +}): Promise<{ + workspace: string + id: string + query: TaskGraphQuery + taskIds: string[] +}> { + const context = resolveMcpWorkspace(input.workspace) + const result = await query_task_graph( + context.workspaceRoot, + input.query, + input.id, + ) + return { + workspace: context.workspaceRoot, + id: input.id, + query: input.query, + taskIds: result.taskIds, + } +} + +export async function listMcpTaskSteps(input: { + workspace: string + id: string +}): Promise<{ workspace: string; id: string; steps: TaskStep[] }> { + const context = resolveMcpWorkspace(input.workspace) + const task = await requireTask(context.workspaceRoot, input.id) + return { workspace: context.workspaceRoot, id: input.id, steps: task.steps } +} + +export async function addMcpTaskSteps(input: { + workspace: string + id: string + steps: TaskStepInput[] +}): Promise<{ workspace: string; task: Task }> { + const context = resolveMcpWorkspace(input.workspace) + const task = await add_steps_for_directory(context.workspaceRoot, { + task_id: input.id, + steps: input.steps.map((step) => ({ + ...step, + summary: step.summary ?? undefined, + notes: step.notes ?? undefined, + })), + }) + return { workspace: context.workspaceRoot, task: map_task(task) } +} + +export async function updateMcpTaskStep(input: { + workspace: string + id: string + stepId: string + patch: Partial<TaskStep> +}): Promise<{ workspace: string; task: Task }> { + const context = resolveMcpWorkspace(input.workspace) + const stepId = parseStepId(input.stepId) + const patch = input.patch + const task = await update_step_for_directory(context.workspaceRoot, { + task_id: input.id, + step_id: stepId, + title: patch.title, + description: patch.description, + status: patch.status, + position: patch.position, + summary: patch.summary ?? undefined, + notes: patch.notes ?? undefined, + }) + return { workspace: context.workspaceRoot, task: map_task(task) } +} + +export async function completeMcpTaskStep(input: { + workspace: string + id: string + stepId?: string + notes?: string +}): Promise<{ workspace: string; task: Task; warnings?: string[] }> { + const context = resolveMcpWorkspace(input.workspace) + const result = await complete_step_for_directory(context.workspaceRoot, { + task_id: input.id, + step_id: input.stepId ? parseStepId(input.stepId) : undefined, + notes: input.notes, + }) + return { + workspace: context.workspaceRoot, + task: map_task(result.task), + warnings: result.warnings, + } +} + +export async function addMcpTaskNote(input: { + workspace: string + id: string + kind: 'discovery' | 'decision' + content: string + source?: 'user' | 'agent' | 'system' + rationale?: string + impact?: string +}): Promise<{ workspace: string; task: Task }> { + const context = resolveMcpWorkspace(input.workspace) + const note = { + task_id: input.id, + content: input.content, + source: input.source ?? 'agent', + rationale: input.rationale, + impact: input.impact, + } + const task = + input.kind === 'discovery' + ? await add_discovery_for_directory(context.workspaceRoot, note) + : await add_decision_for_directory(context.workspaceRoot, note) + return { workspace: context.workspaceRoot, task: map_task(task) } +} diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts new file mode 100644 index 0000000..9b555fd --- /dev/null +++ b/src/mcp/tools.ts @@ -0,0 +1,129 @@ +import { existsSync, statSync } from 'node:fs' +import { isAbsolute, join, resolve } from 'node:path' +import { appendMemory } from '../memory/application/append' +import { + listMemoryEvidence, + searchMemoryEvidence, + type MemoryEvidence, +} from '../memory/application/query' +import { getDbClientByPath } from '../db/client' +import { renderMemorySummary } from '../cli/commands/summary-memory' +import { + loadTaskSummary, + renderTaskSummary, +} from '../cli/commands/summary-tasks' +import { resolveWorkspaceContext } from '../workspace/resolve' +import { runMcpEffect } from './result' + +export type McpWorkspace = { + workspaceRoot: string + memoryDir: string + dbPath: string +} + +export async function getSummary(input: { + workspace: string + taskLimit?: number + memoryLimit?: number +}): Promise<{ workspace: string; output: string }> { + const context = resolveMcpWorkspace(input.workspace) + const taskLimit = input.taskLimit ?? 5 + const memoryLimit = input.memoryLimit ?? 3 + const handle = getDbClientByPath(context.dbPath) + const [tasks, evidence] = await Promise.all([ + loadTaskSummary(taskLimit, context.workspaceRoot), + runMcpEffect(listMemoryEvidence(context.dbPath, {}, handle)), + ]) + const output = [ + '# Continuum Summary', + '', + `Workspace: ${context.workspaceRoot}`, + '', + renderTaskSummary(tasks, taskLimit), + '', + renderMemorySummary(evidence, memoryLimit), + ].join('\n') + return { workspace: context.workspaceRoot, output } +} + +export async function appendMcpMemory(input: { + workspace: string + kind: 'user' | 'agent' | 'tool' + content: string + tags?: string[] +}): Promise<{ + workspace: string + id: string + sequence: number + projectionStale: boolean +}> { + const context = resolveMcpWorkspace(input.workspace) + const result = await runMcpEffect( + appendMemory({ + dbPath: context.dbPath, + nowPath: join(context.memoryDir, 'NOW.md'), + input: { + kind: input.kind, + content: input.content, + metadata: input.tags ? { tags: input.tags } : undefined, + }, + }), + ) + return { + workspace: context.workspaceRoot, + id: result.entry.id, + sequence: result.entry.sequence, + projectionStale: result.projection.stale, + } +} + +export async function searchMcpMemory(input: { + workspace: string + query: string + limit?: number +}): Promise<{ + workspace: string + matches: Array<MemoryEvidence & { score: number }> +}> { + const context = resolveMcpWorkspace(input.workspace) + const handle = getDbClientByPath(context.dbPath) + const matches = await runMcpEffect( + searchMemoryEvidence( + context.dbPath, + input.query, + { limit: input.limit ?? 20 }, + handle, + ), + ) + return { workspace: context.workspaceRoot, matches } +} + +export function resolveMcpWorkspace(workspace: string): McpWorkspace { + if (!isAbsolute(workspace)) { + throw new Error('workspace must be an absolute path') + } + if (!existsSync(workspace) || !statSync(workspace).isDirectory()) { + throw new Error(`workspace directory does not exist: ${workspace}`) + } + const resolved = resolveWorkspaceContext({ startDir: workspace }) + if (!existsSync(resolved.continuumDbPath)) { + throw new Error( + `Continuum is not initialized in workspace: ${resolved.workspaceRoot}`, + ) + } + return { + workspaceRoot: resolved.workspaceRoot, + memoryDir: resolved.memoryDir, + dbPath: resolved.continuumDbPath, + } +} + +export function resolveInitWorkspace(workspace: string): string { + if (!isAbsolute(workspace)) { + throw new Error('workspace must be an absolute path') + } + if (!existsSync(workspace) || !statSync(workspace).isDirectory()) { + throw new Error(`workspace directory does not exist: ${workspace}`) + } + return resolve(workspace) +} diff --git a/src/memory/application/recall-import.ts b/src/memory/application/recall-import.ts index d661f48..cf194b7 100644 --- a/src/memory/application/recall-import.ts +++ b/src/memory/application/recall-import.ts @@ -53,12 +53,13 @@ export function importCanonicalOpencodeRecall( options: CanonicalRecallImportOptions = {}, ): Effect.Effect<CanonicalRecallImportResult, unknown> { return Effect.gen(function* () { - const workspace = getWorkspaceContext() + const workspace = + options.repoPath && options.continuumDbPath ? null : getWorkspaceContext() const extraction = yield* Effect.try({ try: () => (options.extract ?? extractOpencodeSessions)({ dbPath: options.dbPath, - repoPath: options.repoPath ?? workspace.workspaceRoot, + repoPath: options.repoPath ?? workspace!.workspaceRoot, projectId: options.projectId, sessionId: options.sessionId, afterDate: options.afterDate, @@ -69,7 +70,7 @@ export function importCanonicalOpencodeRecall( const repository = options.repository ?? recallRepositoryForPath( - options.continuumDbPath ?? workspace.continuumDbPath, + options.continuumDbPath ?? workspace!.continuumDbPath, ) const config = options.summaryConfig ?? resolveSummaryConfig({}) const sessions = applySessionFilters( diff --git a/src/memory/application/recall-status.ts b/src/memory/application/recall-status.ts new file mode 100644 index 0000000..fa00274 --- /dev/null +++ b/src/memory/application/recall-status.ts @@ -0,0 +1,18 @@ +import type { DbHandle } from '../../db/client' + +export type RecallStatus = { + sources: number + messages: number + summaries: number +} + +export function getRecallStatus(handle: DbHandle): RecallStatus { + return handle.sqlite + .query( + `SELECT + (SELECT COUNT(*) FROM memory_recall_sources) sources, + (SELECT COUNT(*) FROM memory_recall_messages) messages, + (SELECT COUNT(*) FROM memory_recall_summaries) summaries`, + ) + .get() as RecallStatus +} diff --git a/src/memory/config.ts b/src/memory/config.ts index 09da304..1524b63 100644 --- a/src/memory/config.ts +++ b/src/memory/config.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' import { parse } from 'yaml' import { memoryPath } from './paths' @@ -45,8 +46,10 @@ const DEFAULT_CONFIG: MemoryConfig = { memory_sections: [...DEFAULT_SECTIONS, 'Sessions'], } -export function getMemoryConfig(): MemoryConfig { - const configPath = memoryPath('config.yml') +export function getMemoryConfig(memoryDir?: string): MemoryConfig { + const configPath = memoryDir + ? join(memoryDir, 'config.yml') + : memoryPath('config.yml') if (!existsSync(configPath)) { return { ...DEFAULT_CONFIG, diff --git a/src/memory/summarize.ts b/src/memory/summarize.ts index 3cbfb37..489bb30 100644 --- a/src/memory/summarize.ts +++ b/src/memory/summarize.ts @@ -198,7 +198,8 @@ export function mechanicalSummary(body: string): NowSummary { } function extractNarrative(body: string): string { - // Prefer Goal alignment lines, then first ## User: line + // Prefer explicit high-signal context, then retain the source text so the + // no-LLM fallback remains searchable after raw entries are consolidated. const goalMatch = body.match(/^\s*[-*]?\s*Goal alignment\s*:\s*(.+)$/im) if (goalMatch?.[1]) { return goalMatch[1].trim() @@ -207,7 +208,7 @@ function extractNarrative(body: string): string { if (userMatch?.[1]) { return userMatch[1].trim() } - return 'No summary available.' + return body.trim() || 'No summary available.' } function extractMarkers(body: string, pattern: RegExp): string[] { diff --git a/src/skills/setup.ts b/src/skills/setup.ts index bf652a1..6d344a3 100644 --- a/src/skills/setup.ts +++ b/src/skills/setup.ts @@ -36,6 +36,10 @@ export function installSkills( const targetDir = join(targetRoot, '.agents', 'skills') mkdirSync(targetDir, { recursive: true }) + for (const legacySkill of ['continuum-task', 'memory-manager', 'task-loop']) { + rmSync(join(targetDir, legacySkill), { recursive: true, force: true }) + } + const rootSkillFile = join(targetDir, 'SKILL.md') if (existsSync(rootSkillFile) && statSync(rootSkillFile).isFile()) { rmSync(rootSkillFile) diff --git a/tests/mcp-server.test.ts b/tests/mcp-server.test.ts new file mode 100644 index 0000000..a010a4d --- /dev/null +++ b/tests/mcp-server.test.ts @@ -0,0 +1,236 @@ +import { afterEach, describe, expect, test } from 'bun:test' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' + +const repoRoot = process.cwd() +const continuumBin = join(repoRoot, 'bin', 'continuum') +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }) + } +}) + +describe('Continuum MCP server', () => { + test('exposes the MVP tools and preserves formatted memory content', async () => { + const workspace = mkdtempSync(join(tmpdir(), 'continuum-mcp-')) + roots.push(workspace) + + const transport = new StdioClientTransport({ + command: 'bun', + args: ['run', continuumBin, 'mcp'], + cwd: repoRoot, + stderr: 'pipe', + }) + const client = new Client({ name: 'continuum-test', version: '1.0.0' }) + await client.connect(transport) + + try { + const tools = await client.listTools() + expect(tools.tools.map((tool) => tool.name).sort()).toEqual([ + 'continuum_init', + 'continuum_memory_append', + 'continuum_memory_consolidate', + 'continuum_memory_search', + 'continuum_recall_import', + 'continuum_recall_status', + 'continuum_summary', + 'continuum_task_complete', + 'continuum_task_create', + 'continuum_task_delete', + 'continuum_task_get', + 'continuum_task_graph', + 'continuum_task_list', + 'continuum_task_note_add', + 'continuum_task_steps_add', + 'continuum_task_steps_complete', + 'continuum_task_steps_list', + 'continuum_task_steps_update', + 'continuum_task_update', + 'continuum_task_validate', + ]) + expect( + tools.tools.find((tool) => tool.name === 'continuum_summary') + ?.annotations?.readOnlyHint, + ).toBe(true) + expect( + tools.tools.find((tool) => tool.name === 'continuum_summary') + ?.inputSchema.properties?.memoryLimit, + ).toMatchObject({ type: 'integer' }) + + const init = await client.callTool({ + name: 'continuum_init', + arguments: { workspace }, + }) + expect(init.isError).not.toBe(true) + expect(init.structuredContent).toMatchObject({ + workspace, + created: true, + initialized: true, + }) + + const content = + '## Durable entry\n\nUse `code`, "$HOME", $(not-run), and \'quotes\'.\n\n- item one\n- item two' + const append = await client.callTool({ + name: 'continuum_memory_append', + arguments: { + workspace, + kind: 'agent', + content, + tags: ['mcp'], + }, + }) + expect(append.isError).not.toBe(true) + + const search = await client.callTool({ + name: 'continuum_memory_search', + arguments: { workspace, query: 'not-run' }, + }) + expect(search.isError).not.toBe(true) + const searchData = search.structuredContent as { + matches: Array<{ content: string }> + } + expect(searchData.matches[0]?.content).toBe(content) + + const summary = await client.callTool({ + name: 'continuum_summary', + arguments: { workspace }, + }) + expect(summary.isError).not.toBe(true) + const summaryData = summary.structuredContent as { output: string } + expect(summaryData.output).toContain(content) + + const consolidation = await client.callTool({ + name: 'continuum_memory_consolidate', + arguments: { workspace }, + }) + expect(consolidation.structuredContent).toMatchObject({ + workspace, + status: 'completed', + }) + + const recallStatus = await client.callTool({ + name: 'continuum_recall_status', + arguments: { workspace }, + }) + expect(recallStatus.structuredContent).toMatchObject({ + workspace, + sources: 0, + rawMessages: 0, + derivedSummaries: 0, + }) + const recallImport = await client.callTool({ + name: 'continuum_recall_import', + arguments: { workspace, dryRun: true, limit: 1 }, + }) + expect(recallImport.isError).toBe(true) + expect(recallImport.content[0]).toMatchObject({ + type: 'text', + text: expect.stringMatching( + /No OpenCode project found for repo:|OpenCode sqlite database not found:/, + ), + }) + + const created = await client.callTool({ + name: 'continuum_task_create', + arguments: { + workspace, + task: { + title: 'Exercise MCP task tools', + type: 'chore', + description: 'End-to-end MCP coverage', + }, + }, + }) + const task = (created.structuredContent as { task: { id: string } }).task + expect(task.id).toStartWith('tkt-') + + const stepsAdded = await client.callTool({ + name: 'continuum_task_steps_add', + arguments: { + workspace, + id: task.id, + steps: [{ title: 'Verify', description: 'Run MCP checks' }], + }, + }) + const stepId = ( + stepsAdded.structuredContent as { + task: { steps: Array<{ id: string }> } + } + ).task.steps[0]!.id + await client.callTool({ + name: 'continuum_task_steps_update', + arguments: { + workspace, + id: task.id, + stepId, + patch: { status: 'in_progress' }, + }, + }) + await client.callTool({ + name: 'continuum_task_note_add', + arguments: { + workspace, + id: task.id, + kind: 'discovery', + content: 'MCP task notes work.', + }, + }) + await client.callTool({ + name: 'continuum_task_steps_complete', + arguments: { workspace, id: task.id, stepId, notes: 'Verified' }, + }) + const validation = await client.callTool({ + name: 'continuum_task_validate', + arguments: { workspace, id: task.id, transition: 'completed' }, + }) + expect(validation.structuredContent).toMatchObject({ + valid: false, + missingFields: ['plan'], + }) + await client.callTool({ + name: 'continuum_task_update', + arguments: { + workspace, + id: task.id, + patch: { priority: 10, plan: 'Run all MCP operations.' }, + }, + }) + const revalidation = await client.callTool({ + name: 'continuum_task_validate', + arguments: { workspace, id: task.id, transition: 'completed' }, + }) + expect(revalidation.structuredContent).toMatchObject({ valid: true }) + const graph = await client.callTool({ + name: 'continuum_task_graph', + arguments: { workspace, id: task.id, query: 'children' }, + }) + expect(graph.structuredContent).toMatchObject({ taskIds: [] }) + const listed = await client.callTool({ + name: 'continuum_task_list', + arguments: { workspace }, + }) + expect(JSON.stringify(listed.structuredContent)).toContain(task.id) + const fetched = await client.callTool({ + name: 'continuum_task_get', + arguments: { workspace, id: task.id, expand: ['children'] }, + }) + expect(fetched.structuredContent).toMatchObject({ children: [] }) + await client.callTool({ + name: 'continuum_task_complete', + arguments: { workspace, id: task.id, outcome: 'All MCP calls passed.' }, + }) + const deleted = await client.callTool({ + name: 'continuum_task_delete', + arguments: { workspace, id: task.id }, + }) + expect(deleted.structuredContent).toMatchObject({ deleted: true }) + } finally { + await client.close() + } + }, 20_000) +}) diff --git a/tests/memory-config.test.ts b/tests/memory-config.test.ts index 3ce2e9e..c2ecb7b 100644 --- a/tests/memory-config.test.ts +++ b/tests/memory-config.test.ts @@ -78,4 +78,30 @@ describe('memory config', () => { } }) }) + + test('explicit memory directory does not read config from cwd', () => { + withTempCwd(() => { + const cwdMemory = join(process.cwd(), '.continuum', 'memory') + const targetMemory = mkdtempSync( + join(tmpdir(), 'continuum-target-memory-'), + ) + mkdirSync(cwdMemory, { recursive: true }) + writeFileSync( + join(cwdMemory, 'config.yml'), + ['consolidation:', ' api_key: wrong-key', ' model: wrong-model'].join( + '\n', + ), + 'utf-8', + ) + const originalKey = process.env.OPENCODE_ZEN_API_KEY + delete process.env.OPENCODE_ZEN_API_KEY + try { + expect(getMemoryConfig(targetMemory).consolidation).toBeUndefined() + } finally { + rmSync(targetMemory, { recursive: true, force: true }) + if (originalKey !== undefined) + process.env.OPENCODE_ZEN_API_KEY = originalKey + } + }) + }) }) diff --git a/tests/memory-summary-mechanical.test.ts b/tests/memory-summary-mechanical.test.ts new file mode 100644 index 0000000..9552aec --- /dev/null +++ b/tests/memory-summary-mechanical.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from 'bun:test' +import { mechanicalSummary } from '../src/memory/summarize' + +describe('mechanical memory summary', () => { + test('retains agent content for search without an LLM', () => { + const body = [ + '## Agent: Decision: workspace-isolation-marker-7429', + '', + 'Literal `code`, $HOME, and $(not-run).', + ].join('\n') + + const summary = mechanicalSummary(body) + + expect(summary.narrative).toBe(body) + expect(summary.narrative).toContain('workspace-isolation-marker-7429') + expect(summary.narrative).toContain('$(not-run)') + }) +})