From e4732ba3174b2d5adaf586c7b0d4f3a47548574b Mon Sep 17 00:00:00 2001 From: ReBoticsAI Date: Tue, 14 Jul 2026 20:40:36 -0600 Subject: [PATCH 1/2] Migrate marketplace packs to kernel Record bundles. --- .github/workflows/validate.yml | 19 ++++ .gitignore | 1 + CONTRIBUTING.md | 3 +- README.md | 6 ++ catalog/index.json | 8 +- package-lock.json | 92 ++++++++++++++++ package.json | 14 +++ packs/productivity-skills-pack/bundle.json | 57 ++++++---- packs/productivity-skills-pack/manifest.json | 2 +- packs/research-agent-pack/bundle.json | 68 ++++++------ packs/research-agent-pack/manifest.json | 2 +- packs/work-starter-pack/bundle.json | 78 +++++++++----- packs/work-starter-pack/manifest.json | 2 +- schemas/catalog-entry.schema.json | 38 ++++++- schemas/catalog-index.schema.json | 18 ++++ schemas/portable-bundle.schema.json | 92 ++++++++++++++++ scripts/validate-catalog.mjs | 103 ++++++++++++++++++ tests/catalog.test.mjs | 106 +++++++++++++++++++ 18 files changed, 613 insertions(+), 96 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 schemas/catalog-index.schema.json create mode 100644 schemas/portable-bundle.schema.json create mode 100644 scripts/validate-catalog.mjs create mode 100644 tests/catalog.test.mjs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..933eda3 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,19 @@ +name: Validate catalog + +on: + pull_request: + push: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run validate + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d37ff5..7c6fa05 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ - [ ] Entry added to `catalog/index.json` with unique `id` - [ ] `manifest.json` in pack/plugin folder matches schema - [ ] `bundle.json` validates as a GodMode portable bundle (version 1) +- [ ] Bundle records use kernel `Record` payloads (`objectType` + `data`), not legacy table-shaped payloads - [ ] No secrets, API keys, or operator-specific content - [ ] Title and description are clear for OSS users - [ ] Tags help browse/filter (e.g. `work`, `agents`, `skills`) @@ -28,6 +29,6 @@ ## Install types - **clone**: GodMode downloads `bundle.json` and imports via portability API. -- **plugin**: GodMode clones the plugin repo (or uses release tarball), adds to `GODMODE_PLUGIN_PATH`, and runs tenant install. Bridge restart may be required. +- **plugin**: GodMode clones the plugin repo, builds it when needed, loads it into the running Bridge, and runs its tenant install lifecycle. A Bridge restart is not required. Live access to another user's instance is **Shared**, not Marketplace. diff --git a/README.md b/README.md index 43be339..87ac470 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,9 @@ schemas/ JSON Schema for CI validation Default official URL (override with `MARKETPLACE_OFFICIAL_URL` in Bridge): `https://raw.githubusercontent.com/ReBoticsAI/GodMode-Marketplace/main/catalog/index.json` + +## Validate the catalog + +Run `npm install`, then `npm run validate` and `npm test`. Validation checks the +catalog and portable-bundle schemas, manifest/version consistency, kernel Record +shapes, and retired mutation route strings. diff --git a/catalog/index.json b/catalog/index.json index 8d304ca..874153d 100644 --- a/catalog/index.json +++ b/catalog/index.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "repoBase": "https://raw.githubusercontent.com/ReBoticsAI/GodMode-Marketplace/main", "updatedAt": "2026-07-14T00:00:00.000Z", "entries": [ @@ -9,7 +9,7 @@ "installType": "clone", "title": "Research Agent Pack", "description": "A Research Agent subagent plus a research workflow skill for Intelligence.", - "version": "1.0.0", + "version": "1.1.0", "author": "GodMode", "tags": ["agents", "research", "skills"], "bundlePath": "packs/research-agent-pack/bundle.json" @@ -20,7 +20,7 @@ "installType": "clone", "title": "Work Starter Department", "description": "Work department with Projects division and a sample project page.", - "version": "1.0.0", + "version": "1.1.0", "author": "GodMode", "tags": ["structure", "work"], "bundlePath": "packs/work-starter-pack/bundle.json" @@ -31,7 +31,7 @@ "installType": "clone", "title": "Productivity Skills", "description": "Weekly review and meeting prep skills for Intelligence.", - "version": "1.0.0", + "version": "1.1.0", "author": "GodMode", "tags": ["skills", "productivity"], "bundlePath": "packs/productivity-skills-pack/bundle.json" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7534912 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,92 @@ +{ + "name": "godmode-marketplace", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "godmode-marketplace", + "version": "1.0.0", + "devDependencies": { + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b42bcc5 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "godmode-marketplace", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "validate": "node scripts/validate-catalog.mjs", + "test": "node --test tests/*.test.mjs" + }, + "devDependencies": { + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + } +} diff --git a/packs/productivity-skills-pack/bundle.json b/packs/productivity-skills-pack/bundle.json index 08136da..e99582f 100644 --- a/packs/productivity-skills-pack/bundle.json +++ b/packs/productivity-skills-pack/bundle.json @@ -1,48 +1,59 @@ { "version": 1, "kind": "bundle", - "exportedAt": "2026-07-04T00:00:00.000Z", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "productivity-skills-pack", "title": "Productivity Skills", "data": { "children": [ { "version": 1, - "kind": "skill", - "exportedAt": "2026-07-04T00:00:00.000Z", + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "weekly-review", "title": "Weekly Review", "data": { - "skill": { + "record": { "id": "weekly-review", - "agent_id": "intelligence", - "name": "weekly-review", - "description": "End-of-week review across tasks, calendar, and wiki", - "body": "1. List open tasks from the kanban.\n2. Summarize calendar events this week.\n3. Note wiki updates.\n4. Propose next-week priorities.", - "tools_json": "[\"list_project_cards\",\"list_calendar_events\"]", - "departments_json": "[]", - "enabled": 1, - "status": "active" + "objectType": "Skill", + "data": { + "id": "weekly-review", + "agent_id": "intelligence", + "name": "weekly-review", + "description": "End-of-week review across tasks, calendar, and wiki", + "body": "1. List open tasks from the kanban.\n2. Summarize calendar events this week.\n3. Note wiki updates.\n4. Propose next-week priorities.", + "tools_json": [ + "list_project_cards", + "list_calendar_events" + ], + "departments_json": [], + "enabled": true, + "status": "active" + } } } }, { "version": 1, - "kind": "skill", - "exportedAt": "2026-07-04T00:00:00.000Z", + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "meeting-prep", "title": "Meeting Prep", "data": { - "skill": { + "record": { "id": "meeting-prep", - "agent_id": "intelligence", - "name": "meeting-prep", - "description": "Prepare agenda and context before a meeting", - "body": "1. Ask for meeting title and attendees.\n2. Pull related wiki pages and open tasks.\n3. Draft a short agenda and talking points.", - "tools_json": "[]", - "departments_json": "[]", - "enabled": 1, - "status": "active" + "objectType": "Skill", + "data": { + "id": "meeting-prep", + "agent_id": "intelligence", + "name": "meeting-prep", + "description": "Prepare agenda and context before a meeting", + "body": "1. Ask for meeting title and attendees.\n2. Pull related wiki pages and open tasks.\n3. Draft a short agenda and talking points.", + "tools_json": [], + "departments_json": [], + "enabled": true, + "status": "active" + } } } } diff --git a/packs/productivity-skills-pack/manifest.json b/packs/productivity-skills-pack/manifest.json index 06a818b..f78aa42 100644 --- a/packs/productivity-skills-pack/manifest.json +++ b/packs/productivity-skills-pack/manifest.json @@ -3,6 +3,6 @@ "kind": "bundle", "title": "Productivity Skills", "description": "Weekly review and meeting prep skills.", - "version": "1.0.0", + "version": "1.1.0", "author": "GodMode" } diff --git a/packs/research-agent-pack/bundle.json b/packs/research-agent-pack/bundle.json index 19cf52c..e4b4653 100644 --- a/packs/research-agent-pack/bundle.json +++ b/packs/research-agent-pack/bundle.json @@ -1,61 +1,55 @@ { "version": 1, "kind": "bundle", - "exportedAt": "2026-07-04T00:00:00.000Z", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "research-agent-pack", "title": "Research Agent Pack", "data": { "children": [ { "version": 1, - "kind": "agent", - "exportedAt": "2026-07-04T00:00:00.000Z", + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "research-agent", "title": "Research Agent", "data": { - "agent": { + "record": { "id": "research-agent", - "name": "Research Agent", - "description": "Finds sources, summarizes findings, and saves notes to the wiki.", - "icon": "search", - "backend": "local", - "enabled": 1, - "is_template": 0, - "system_prompt": "You are a research specialist. Search, summarize, and cite sources. Save durable findings to the wiki.", - "sampling_json": "{\"temperature\":0.4,\"topP\":0.9,\"maxTokens\":4096}", - "thinking_json": "{\"enabled\":false}", - "tool_allow_json": null, - "auto_approve_json": "[]", - "model_path": null, - "adapter_ids_json": "[]", - "config_json": "{}", - "parent_id": "intelligence", - "team": "research" - }, - "memories": [], - "ruleState": [], - "skillState": [], - "artifacts": [], - "workflows": [] + "objectType": "Agent", + "data": { + "id": "research-agent", + "name": "Research Agent", + "description": "Finds sources, summarizes findings, and saves notes to the wiki.", + "icon": "search", + "backend": "local", + "enabled": true, + "parent_id": "intelligence", + "team": "research" + } + } } }, { "version": 1, - "kind": "skill", - "exportedAt": "2026-07-04T00:00:00.000Z", + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "deep-research", "title": "Deep Research", "data": { - "skill": { + "record": { "id": "deep-research", - "agent_id": "intelligence", - "name": "deep-research", - "description": "Structured research workflow with sources and wiki output", - "body": "1. Clarify the research question.\n2. Search and gather 3-5 sources.\n3. Summarize in bullets with citations.\n4. Offer to create a wiki page with findings.", - "tools_json": "[]", - "departments_json": "[]", - "enabled": 1, - "status": "active" + "objectType": "Skill", + "data": { + "id": "deep-research", + "agent_id": "intelligence", + "name": "deep-research", + "description": "Structured research workflow with sources and wiki output", + "body": "1. Clarify the research question.\n2. Search and gather 3-5 sources.\n3. Summarize in bullets with citations.\n4. Offer to create a wiki page with findings.", + "tools_json": [], + "departments_json": [], + "enabled": true, + "status": "active" + } } } } diff --git a/packs/research-agent-pack/manifest.json b/packs/research-agent-pack/manifest.json index 38a628f..1ee16f7 100644 --- a/packs/research-agent-pack/manifest.json +++ b/packs/research-agent-pack/manifest.json @@ -3,6 +3,6 @@ "kind": "bundle", "title": "Research Agent Pack", "description": "Research Agent subagent and deep-research skill.", - "version": "1.0.0", + "version": "1.1.0", "author": "GodMode" } diff --git a/packs/work-starter-pack/bundle.json b/packs/work-starter-pack/bundle.json index 84489ae..0e61366 100644 --- a/packs/work-starter-pack/bundle.json +++ b/packs/work-starter-pack/bundle.json @@ -1,41 +1,69 @@ { "version": 1, "kind": "bundle", - "exportedAt": "2026-07-04T00:00:00.000Z", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "work-starter-pack", "title": "Work Starter Department", "data": { "children": [ { "version": 1, - "kind": "department", - "exportedAt": "2026-07-04T00:00:00.000Z", + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", "sourceId": "work", "title": "Work", "data": { - "department": { + "record": { "id": "work", - "label": "Work", - "icon": "briefcase", - "basePath": "/work", - "sortOrder": 1, - "divisions": [ - { - "id": "projects", - "label": "Projects", - "icon": "folder-kanban", - "sortOrder": 1, - "pages": [ - { - "id": "sample-project", - "label": "Sample Project", - "icon": "file-text", - "kind": "custom", - "sortOrder": 1 - } - ] - } - ] + "objectType": "StructureNode", + "data": { + "id": "work", + "parent_id": null, + "label": "Work", + "icon": "briefcase", + "segment": "work" + } + } + } + }, + { + "version": 1, + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", + "sourceId": "work-projects", + "title": "Projects", + "data": { + "record": { + "id": "work-projects", + "objectType": "StructureNode", + "data": { + "id": "work-projects", + "parent_id": "work", + "label": "Projects", + "icon": "folder-kanban", + "segment": "projects" + } + } + } + }, + { + "version": 1, + "kind": "record", + "exportedAt": "2026-07-14T00:00:00.000Z", + "sourceId": "work-projects-sample-project", + "title": "Sample Project", + "data": { + "record": { + "id": "work-projects-sample-project", + "objectType": "StructureNode", + "data": { + "id": "work-projects-sample-project", + "parent_id": "work-projects", + "label": "Sample Project", + "icon": "file-text", + "segment": "sample-project", + "kind": "placeholder" + } } } } diff --git a/packs/work-starter-pack/manifest.json b/packs/work-starter-pack/manifest.json index 6703710..3c6bf9c 100644 --- a/packs/work-starter-pack/manifest.json +++ b/packs/work-starter-pack/manifest.json @@ -3,6 +3,6 @@ "kind": "bundle", "title": "Work Starter Department", "description": "Work department with Projects division and sample page.", - "version": "1.0.0", + "version": "1.1.0", "author": "GodMode" } diff --git a/schemas/catalog-entry.schema.json b/schemas/catalog-entry.schema.json index 60f59c5..0cd0366 100644 --- a/schemas/catalog-entry.schema.json +++ b/schemas/catalog-entry.schema.json @@ -1,20 +1,52 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/ReBoticsAI/GodMode-Marketplace/schemas/catalog-entry.schema.json", "title": "GodModeCatalogEntry", "type": "object", "required": ["id", "kind", "installType", "title", "description", "version", "author"], "properties": { "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, - "kind": { "type": "string" }, + "kind": { "enum": ["bundle", "plugin"] }, "installType": { "enum": ["clone", "plugin"] }, "title": { "type": "string", "minLength": 1 }, "description": { "type": "string" }, - "version": { "type": "string" }, + "version": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" + }, "author": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } }, "bundlePath": { "type": "string" }, "pluginRepo": { "type": "string", "format": "uri" }, "pluginRef": { "type": "string" }, "previewPath": { "type": "string" } - } + }, + "allOf": [ + { + "if": { + "properties": { "installType": { "const": "clone" } }, + "required": ["installType"] + }, + "then": { + "required": ["bundlePath"], + "not": { + "anyOf": [ + { "required": ["pluginRepo"] }, + { "required": ["pluginRef"] } + ] + } + } + }, + { + "if": { + "properties": { "installType": { "const": "plugin" } }, + "required": ["installType"] + }, + "then": { + "required": ["pluginRepo"], + "not": { "required": ["bundlePath"] } + } + } + ], + "additionalProperties": false } diff --git a/schemas/catalog-index.schema.json b/schemas/catalog-index.schema.json new file mode 100644 index 0000000..97e003e --- /dev/null +++ b/schemas/catalog-index.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/ReBoticsAI/GodMode-Marketplace/schemas/catalog-index.schema.json", + "title": "GodModeCatalogIndex", + "type": "object", + "required": ["version", "repoBase", "updatedAt", "entries"], + "properties": { + "version": { "type": "integer", "minimum": 1 }, + "repoBase": { "type": "string", "format": "uri" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "entries": { + "type": "array", + "minItems": 1, + "items": { "$ref": "catalog-entry.schema.json" } + } + }, + "additionalProperties": false +} diff --git a/schemas/portable-bundle.schema.json b/schemas/portable-bundle.schema.json new file mode 100644 index 0000000..f40b9c5 --- /dev/null +++ b/schemas/portable-bundle.schema.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/ReBoticsAI/GodMode-Marketplace/schemas/portable-bundle.schema.json", + "title": "GodModePortableBundleV1", + "oneOf": [ + { "$ref": "#/definitions/bundle" }, + { "$ref": "#/definitions/recordEntity" } + ], + "definitions": { + "base": { + "type": "object", + "required": [ + "version", + "kind", + "exportedAt", + "sourceId", + "title", + "data" + ], + "properties": { + "version": { "const": 1 }, + "kind": { "type": "string" }, + "exportedAt": { "type": "string", "format": "date-time" }, + "sourceId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$" + }, + "title": { "type": "string", "minLength": 1 }, + "data": { "type": "object" } + }, + "additionalProperties": false + }, + "bundle": { + "allOf": [ + { "$ref": "#/definitions/base" }, + { + "properties": { + "kind": { "const": "bundle" }, + "data": { + "type": "object", + "required": ["children"], + "properties": { + "children": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/recordEntity" } + } + }, + "additionalProperties": false + } + } + } + ] + }, + "recordEntity": { + "allOf": [ + { "$ref": "#/definitions/base" }, + { + "properties": { + "kind": { "const": "record" }, + "data": { + "type": "object", + "required": ["record"], + "properties": { + "record": { "$ref": "#/definitions/record" } + }, + "additionalProperties": false + } + } + } + ] + }, + "record": { + "type": "object", + "required": ["id", "objectType", "data"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$" + }, + "objectType": { + "enum": ["StructureNode", "Agent", "Skill"] + }, + "data": { + "type": "object", + "required": ["id"] + } + }, + "additionalProperties": false + } + } +} diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs new file mode 100644 index 0000000..de34f8f --- /dev/null +++ b/scripts/validate-catalog.mjs @@ -0,0 +1,103 @@ +import assert from "node:assert/strict"; +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const json = async (relativePath) => + JSON.parse(await readFile(path.join(root, relativePath), "utf8")); + +const entrySchema = await json("schemas/catalog-entry.schema.json"); +const indexSchema = await json("schemas/catalog-index.schema.json"); +const bundleSchema = await json("schemas/portable-bundle.schema.json"); + +const ajv = new Ajv({ allErrors: true, strict: false }); +addFormats(ajv); +ajv.addSchema(entrySchema); +const validateIndex = ajv.compile(indexSchema); +const validateBundle = ajv.compile(bundleSchema); + +const catalog = await json("catalog/index.json"); +assert( + validateIndex(catalog), + `catalog/index.json failed schema validation:\n${ajv.errorsText(validateIndex.errors, { + separator: "\n", + })}` +); + +const ids = catalog.entries.map((entry) => entry.id); +assert.equal(new Set(ids).size, ids.length, "catalog entry ids must be unique"); + +for (const entry of catalog.entries) { + const manifest = await json( + `${entry.installType === "clone" ? "packs" : "plugins"}/${entry.id}/manifest.json` + ); + assert.equal(manifest.id, entry.id, `${entry.id}: manifest id must match catalog`); + assert.equal( + manifest.version, + entry.version, + `${entry.id}: manifest and catalog versions must match` + ); + assert.equal(manifest.kind, entry.kind, `${entry.id}: manifest kind must match catalog`); + + if (entry.installType !== "clone") continue; + const bundle = await json(entry.bundlePath); + assert( + validateBundle(bundle), + `${entry.bundlePath} failed schema validation:\n${ajv.errorsText( + validateBundle.errors, + { separator: "\n" } + )}` + ); + assert.equal(bundle.sourceId, entry.id, `${entry.id}: bundle sourceId must match catalog`); + + for (const child of bundle.data.children) { + const { record } = child.data; + assert.equal(record.id, child.sourceId, `${entry.id}: record id must match sourceId`); + assert.equal( + record.data.id, + record.id, + `${entry.id}: Record.data.id must match Record.id` + ); + } +} + +const retiredRouteFragments = [ + ["/api", "departments"], + ["/api", "divisions"], + ["/api", "structure"], + ["/api", "ai", "agents"], + ["/api", "ai", "skills"], +].map((parts) => parts.join("/")); +const scanRoots = ["catalog", "packs", "plugins"]; + +async function textFiles(directory) { + const entries = await readdir(path.join(root, directory), { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const relative = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...(await textFiles(relative))); + else if (/\.(?:json|md|mjs|js|ts|tsx|ya?ml)$/.test(entry.name)) files.push(relative); + } + return files; +} + +const scannedFiles = [ + ...(await Promise.all(scanRoots.map(textFiles))).flat(), + "README.md", + "CONTRIBUTING.md", +]; +for (const file of scannedFiles) { + const source = await readFile(path.join(root, file), "utf8"); + for (const route of retiredRouteFragments) { + assert(!source.includes(route), `${file} contains retired mutation route ${route}`); + } +} + +console.log( + `Validated ${catalog.entries.length} catalog entries and ${ + catalog.entries.filter((entry) => entry.installType === "clone").length + } kernel-native packs.` +); diff --git a/tests/catalog.test.mjs b/tests/catalog.test.mjs new file mode 100644 index 0000000..8dbee1a --- /dev/null +++ b/tests/catalog.test.mjs @@ -0,0 +1,106 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const json = async (relativePath) => + JSON.parse(await readFile(path.join(root, relativePath), "utf8")); + +const packPaths = [ + "packs/research-agent-pack/bundle.json", + "packs/work-starter-pack/bundle.json", + "packs/productivity-skills-pack/bundle.json", +]; + +test("catalog and pack manifests publish the same versions", async () => { + const catalog = await json("catalog/index.json"); + assert.equal(catalog.version, 2); + + for (const entry of catalog.entries.filter(({ installType }) => installType === "clone")) { + const manifest = await json(`packs/${entry.id}/manifest.json`); + assert.equal(entry.version, "1.1.0"); + assert.equal(manifest.version, entry.version); + } +}); + +test("all pack children are kernel Record payloads", async () => { + for (const packPath of packPaths) { + const bundle = await json(packPath); + assert.equal(bundle.version, 1); + assert.equal(bundle.kind, "bundle"); + + for (const child of bundle.data.children) { + assert.equal(child.kind, "record"); + assert.deepEqual(Object.keys(child.data), ["record"]); + const { record } = child.data; + assert.match(record.id, /^[a-z0-9][a-z0-9-]*$/); + assert.ok(["StructureNode", "Agent", "Skill"].includes(record.objectType)); + assert.equal(record.data.id, record.id); + assert.equal(child.sourceId, record.id); + assert.equal(typeof record.data, "object"); + assert.ok(!Array.isArray(record.data)); + } + } +}); + +test("work starter pack is a parent-first StructureNode tree", async () => { + const bundle = await json("packs/work-starter-pack/bundle.json"); + const records = bundle.data.children.map(({ data }) => data.record); + assert.deepEqual( + records.map(({ id }) => id), + ["work", "work-projects", "work-projects-sample-project"] + ); + assert.ok(records.every(({ objectType }) => objectType === "StructureNode")); + assert.deepEqual( + records.map(({ data }) => data.parent_id), + [null, "work", "work-projects"] + ); + assert.equal(records[2].data.kind, "placeholder"); + assert.ok(records.every(({ data }) => !("sort_order" in data))); +}); + +test("Agent and Skill records use Record API value types", async () => { + const bundles = await Promise.all([ + json("packs/research-agent-pack/bundle.json"), + json("packs/productivity-skills-pack/bundle.json"), + ]); + const records = bundles.flatMap(({ data }) => + data.children.map((child) => child.data.record) + ); + const agent = records.find(({ objectType }) => objectType === "Agent"); + const skills = records.filter(({ objectType }) => objectType === "Skill"); + + assert.ok(agent); + assert.equal(typeof agent.data.enabled, "boolean"); + assert.equal(skills.length, 3); + for (const skill of skills) { + assert.equal(typeof skill.data.enabled, "boolean"); + assert.ok(Array.isArray(skill.data.tools_json)); + assert.ok(Array.isArray(skill.data.departments_json)); + } +}); + +test("packs contain no legacy persistence envelopes or retired route strings", async () => { + const retiredRoutes = [ + ["/api", "departments"], + ["/api", "divisions"], + ["/api", "structure"], + ["/api", "ai", "agents"], + ["/api", "ai", "skills"], + ].map((parts) => parts.join("/")); + + for (const packPath of packPaths) { + const source = await readFile(path.join(root, packPath), "utf8"); + const parsed = JSON.parse(source); + assert.ok(!source.includes('"kind": "department"')); + assert.ok(!source.includes('"kind": "agent"')); + assert.ok(!source.includes('"kind": "skill"')); + assert.ok(!source.includes('"department":')); + assert.ok(!source.includes('"agent":')); + assert.ok(!source.includes('"skill":')); + for (const route of retiredRoutes) assert.ok(!source.includes(route)); + assert.ok(parsed.data.children.every(({ kind }) => kind === "record")); + } +}); From 8009e49f6bd7ffc9402f77bbede893453e846461 Mon Sep 17 00:00:00 2001 From: ReBoticsAI Date: Tue, 14 Jul 2026 21:42:02 -0600 Subject: [PATCH 2/2] Document marketplace kernel contracts. Clarify Record bundle invariants, plugin API coordination, install behavior, and the validation boundary so contributors test against compatible hosts. --- CONTRIBUTING.md | 50 ++++++++++++++++++++++++++++++++++++++++++------ README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c6fa05..f722ccf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,19 +3,29 @@ ## PR checklist - [ ] Entry added to `catalog/index.json` with unique `id` -- [ ] `manifest.json` in pack/plugin folder matches schema -- [ ] `bundle.json` validates as a GodMode portable bundle (version 1) -- [ ] Bundle records use kernel `Record` payloads (`objectType` + `data`), not legacy table-shaped payloads +- [ ] Local `manifest.json` id, kind, and version match the catalog entry +- [ ] Clone entry has a version-1 `kind: "bundle"` `bundle.json` at `bundlePath` +- [ ] Every bundle child is a version-1 `kind: "record"` envelope whose only data member is `record` +- [ ] Every Record has `id`, `objectType`, and `data`; Record `data.id`, Record `id`, and child `sourceId` match +- [ ] Records use current kernel value shapes, not legacy table/envelope shapes or retired mutation routes +- [ ] Parent/dependency Records precede children that reference them +- [ ] Plugin source contains a valid `godmode.plugin.json`; executable Bridge/web plugins declare `kernelApiVersion: 1` and use `api.kernel` +- [ ] Plugin catalog, local manifest, and source `godmode.plugin.json` versions are coordinated - [ ] No secrets, API keys, or operator-specific content - [ ] Title and description are clear for OSS users - [ ] Tags help browse/filter (e.g. `work`, `agents`, `skills`) +The `manifest.json` beside a pack or plugin in this repository is catalog +metadata, not the plugin runtime manifest. Runtime fields such as +`kernelApiVersion`, `bridge`, `web`, `objectTypes`, and `records` belong in the +referenced source repository's `godmode.plugin.json`. + ## Catalog entry fields | Field | Required | Description | |-------|----------|-------------| | `id` | yes | Stable slug (kebab-case) | -| `kind` | yes | `bundle`, `skill`, `rule`, `agent`, `department`, `plugin`, etc. | +| `kind` | yes | `bundle` or `plugin` | | `installType` | yes | `clone` (portable bundle) or `plugin` (GitHub repo) | | `title` | yes | Display name | | `description` | yes | Short summary | @@ -28,7 +38,35 @@ ## Install types -- **clone**: GodMode downloads `bundle.json` and imports via portability API. -- **plugin**: GodMode clones the plugin repo, builds it when needed, loads it into the running Bridge, and runs its tenant install lifecycle. A Bridge restart is not required. +- **clone**: GodMode resolves and fetches `bundlePath`, then passes the + version-1 portable bundle to its importer. Bundle children are imported in + array order, so order dependencies explicitly. +- **plugin**: GodMode clones the requested `pluginRef` (or updates an existing + checkout), builds a missing Bridge entry, validates and loads + `godmode.plugin.json`, persists the discovery path, and installs the plugin + for the current tenant. The install path returns `restartRequired: false`. + ObjectTypes register before Record seeds and the target plugin's + `tenant:install` hook; plugin knowledge is synchronized afterward. + +Manifest-only plugins may declare `objectTypes` and deterministic-id `records` +without `bridge.entry`. Executable Bridge/web plugins receive versioned kernel +clients through `api.kernel`; both currently report `apiVersion: 1`. Declare +`kernelApiVersion: 1` in `godmode.plugin.json`. A different declared version is +rejected during host manifest validation. + +## Validation + +Use Node 22 and run `npm ci`, `npm run validate`, and `npm test` before opening +a PR. The catalog validator checks local JSON schemas and cross-file identity, +version, bundle, Record-id, and retired-route invariants. Tests cover the exact +published pack set and current Record value shapes. + +The marketplace checks do **not** fetch `pluginRepo`, validate its +`godmode.plugin.json`, execute a clone bundle import, or run a plugin's +build/install lifecycle. Test clone packs with a host importer that supports +`kind: "record"` children. For plugin entries, also test the source repository +against the coordinated GodMode host version, including manifest validation, +ObjectType/Record registration, tenant install, uninstall/reinstall, and tenant +isolation. Live access to another user's instance is **Shared**, not Marketplace. diff --git a/README.md b/README.md index 87ac470..6b078e0 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,36 @@ Private plugins stay on your GitHub account and are added in GodMode under **Mar ``` catalog/index.json Official listing index -packs// Portable bundles (clone install) -plugins// Plugin catalog entries (repo pointers) +packs// Catalog manifest + portable Record bundle +plugins// Catalog manifest (source-repository pointer) schemas/ JSON Schema for CI validation ``` +Pack `manifest.json` files mirror catalog identity and version metadata. Their +`bundle.json` install payload is a version-1 `kind: "bundle"` envelope whose +`data.children` are version-1 `kind: "record"` envelopes. Each child contains +only `data.record`, with: + +- a stable `id`; +- an `objectType` currently accepted by this catalog (`StructureNode`, `Agent`, + or `Skill`); and +- Record `data` whose `id` matches both the Record id and child `sourceId`. + +Keep dependency-sensitive children in import order (for example, a parent +`StructureNode` before its descendants). Clone installation fetches the +catalog entry's `bundlePath` and submits that portable bundle to GodMode's +importer, which processes bundle children in order. These migrated packs +therefore require a coordinated host whose portability importer supports +`kind: "record"` children. + +Plugin `manifest.json` files in this repository are also catalog mirrors; they +are not the executable plugin manifest. The referenced plugin repository must +contain a valid `godmode.plugin.json`. Bridge/web plugin code should declare +`kernelApiVersion: 1` there and use the versioned `api.kernel` client +(`api.kernel.apiVersion === 1`). The host rejects a declared unsupported future +kernel API version instead of guessing compatibility. Keep the catalog entry, +this repository's manifest, and the source plugin version coordinated. + ## Catalog URL Default official URL (override with `MARKETPLACE_OFFICIAL_URL` in Bridge): @@ -28,6 +53,22 @@ Default official URL (override with `MARKETPLACE_OFFICIAL_URL` in Bridge): ## Validate the catalog -Run `npm install`, then `npm run validate` and `npm test`. Validation checks the -catalog and portable-bundle schemas, manifest/version consistency, kernel Record -shapes, and retired mutation route strings. +Use Node 22 (the CI version), then run: + +``` +npm ci +npm run validate +npm test +``` + +`npm run validate` checks the catalog and portable-bundle schemas, unique +catalog ids, catalog/local-manifest id-kind-version agreement, bundle +`sourceId`, Record/child id agreement, and retired mutation route strings. The +tests assert the published pack versions and the current `StructureNode`, +`Agent`, and `Skill` Record value shapes. + +These repository checks do not execute a host import, clone plugin repositories, +or validate their `godmode.plugin.json`, build, registrations, migrations, +hooks, or `kernelApiVersion`. Test clone installation and plugin submissions +against the coordinated GodMode host version; validate plugin runtime behavior +in the source repository as well.