diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..170d186 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": "mcode-stack", + "description": "Workflows and skills the MiniMax Code team uses to build MiniMax Code. Each stack installs as one plugin.", + "owner": { + "name": "MiniMax Code team", + "url": "https://github.com/MiniMax-AI/mcode-stack" + }, + "plugins": [ + { + "name": "ronny", + "source": "./stacks/ronny", + "description": "Daily workflows and self-check skills from the MiniMax Code R&D lead: independent review, subagent split checks, tool and context budget audits, long sessions, long commands, harness experiments, trajectory review, plus everyday engineering practice for code review, review comments, small PRs, change descriptions and emergency changes.", + "category": "Code", + "author": { + "name": "Ronny (MiniMax Code team)" + } + } + ] +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 98a4740..b894d9b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -13,6 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + # Full history, so the version bump check can find the merge base. + fetch-depth: 0 - name: Fetch the official plugin validator from MiniMax Code uses: actions/checkout@v4 @@ -31,4 +34,6 @@ jobs: - name: Check all stacks env: MINIMAX_CODE_DIR: .minimax-code + # Empty on pushes to main, which skips the version bump check. + CHECK_BASE: ${{ github.event.pull_request.base.sha }} run: node scripts/check.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbb4de7..9c347f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,12 @@ 2. **Copy the template** to `community//`. The `id` uses only lowercase letters, digits and hyphens, and must equal the `name` in `.minimax-plugin/plugin.json`. -3. **Run the checks locally** and paste the output into the PR: +3. **List it in the marketplace**: add an entry to + `.claude-plugin/marketplace.json` with `"name"` equal to the id, + `"source": "./community/"` and the same `description` as your + `plugin.json`. Do not add `skills` or other component fields; your + `plugin.json` defines them. +4. **Run the checks locally** and paste the output into the PR: ```bash node scripts/check.mjs @@ -27,10 +32,18 @@ MINIMAX_CODE_DIR=../minimax-code node scripts/check.mjs ``` -4. **Include a real usage record** in the PR: the task where the agent used +5. **Include a real usage record** in the PR: the task where the agent used the skill, what the process looked like, and how it turned out. Show the process, not just the final result. +## Changing a stack + +Whenever a PR changes anything inside a stack, raise `version` in its +`.minimax-plugin/plugin.json` (for example `0.2.0` to `0.2.1`). People who +installed the stack from the marketplace only get the update when the version +goes up, so CI fails a change without a bump. Run the same check locally with +`CHECK_BASE=origin/main node scripts/check.mjs`. + ## How we review At least one maintainer reviews each PR, looking at three things: diff --git a/CONTRIBUTING_ZH.md b/CONTRIBUTING_ZH.md index 92ad4fe..9b90401 100644 --- a/CONTRIBUTING_ZH.md +++ b/CONTRIBUTING_ZH.md @@ -12,7 +12,8 @@ 1. **先开一个「New stack」issue**,用几句话说清楚你是谁、怎么用 agent、打算放哪几样东西。 2. **从模板复制**到 `community//`。`id` 只用小写字母、数字和连字符,并且要和 `.minimax-plugin/plugin.json` 里的 `name` 一致。 -3. **本地跑校验**,把输出贴进 PR: +3. **登记到插件市场**:在 `.claude-plugin/marketplace.json` 里加一条,`"name"` 填 id,`"source"` 填 `"./community/"`,`description` 和 `plugin.json` 里的保持一致。不要写 `skills` 等组件字段,这些由 `plugin.json` 定义。 +4. **本地跑校验**,把输出贴进 PR: ```bash node scripts/check.mjs @@ -21,7 +22,11 @@ MINIMAX_CODE_DIR=../minimax-code node scripts/check.mjs ``` -4. **PR 里附一段真实的使用记录**:agent 在什么任务里用了这个 Skill,过程是什么样,结果怎么样。不要只贴最终结果。 +5. **PR 里附一段真实的使用记录**:agent 在什么任务里用了这个 Skill,过程是什么样,结果怎么样。不要只贴最终结果。 + +## 修改已有的 stack + +PR 只要改了某份 stack 里的文件,就要同时调高它 `.minimax-plugin/plugin.json` 里的 `version`(比如 `0.2.0` 改成 `0.2.1`)。从插件市场装的用户,只有版本号变高才会收到更新,所以不调版本号的改动 CI 会直接报错。本地可以用 `CHECK_BASE=origin/main node scripts/check.mjs` 跑同样的检查。 ## 怎么审 diff --git a/README.md b/README.md index 3888d1e..54ddad0 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,21 @@ Copy the whole `stacks/ronny` directory into that directory, then open installed as soon as it is in that directory; press Space in the panel to enable or disable it. +**Every stack, from the marketplace:** + +This repository is also a plugin marketplace, so you can install a stack +without copying files and pick up updates later. In MiniMax Code desktop, open +**Plugins**, choose **Add marketplace** and enter `MiniMax-AI/mcode-stack`; each +stack then shows up as one plugin you can install, update or uninstall. Adding +marketplaces is rolling out gradually, so your version may not have it yet. + +The same catalog works in Claude Code: + +```bash +/plugin marketplace add MiniMax-AI/mcode-stack +/plugin install ronny@mcode-stack +``` +
More install options diff --git a/README_ZH.md b/README_ZH.md index 31fd267..1a12ea4 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -42,6 +42,17 @@ mcode plugin marketplace list # 查看本地插件目录 把 `stacks/ronny` 整个目录复制到那个目录下,然后在 MiniMax Code 里打开 `/plugins`,按 `Ctrl+R` 刷新。本地插件放进去就算装好了,可以在面板里按空格启用或停用。 +**从插件市场安装:** + +这个仓库本身也是一个插件市场,不用复制文件就能装,之后有更新也能直接拿到。在 MiniMax Code 桌面端打开「插件」,点「添加市场」,填入 `MiniMax-AI/mcode-stack`,每份 stack 都会显示成一个插件,可以安装、更新或卸载。添加市场的功能正在逐步开放,你的版本可能还没有。 + +Claude Code 也能用同一份目录: + +```bash +/plugin marketplace add MiniMax-AI/mcode-stack +/plugin install ronny@mcode-stack +``` +
更多安装方式 diff --git a/scripts/check.mjs b/scripts/check.mjs index c34ddd6..fd1fb79 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -5,12 +5,22 @@ // the directory, the icon and every referenced SKILL.md exist, every skill // directory is listed in the manifest, and every SKILL.md has frontmatter // with a `name` equal to its directory and a non-empty `description`. -// 2. Official MiniMax Code plugin validator: if MINIMAX_CODE_DIR points at a +// 2. Marketplace: .claude-plugin/marketplace.json has exactly one entry per +// stack under stacks/ and community/, named after its directory, with the +// same description as its plugin.json and no component fields (the stack's +// own plugin.json defines its skills). +// 3. Version bump: if CHECK_BASE names a commit, every stack whose files +// changed since its merge base with HEAD (uncommitted changes included) +// must have a higher plugin.json "version" than at that merge base. +// Marketplace installs look at the version to decide whether an update is +// available, so a change without a bump never reaches installed copies. +// 4. Official MiniMax Code plugin validator: if MINIMAX_CODE_DIR points at a // checkout of https://github.com/MiniMax-AI/minimax-code (only // tools/plugins and schemas/plugins are needed), each stack is also run // through tools/plugins/validate-plugin.mjs. // -// Usage: node scripts/check.mjs (lint only) +// Usage: node scripts/check.mjs (lint and marketplace) +// CHECK_BASE=origin/main node scripts/check.mjs (plus the version bump check) // MINIMAX_CODE_DIR=../minimax-code node scripts/check.mjs import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; @@ -20,9 +30,15 @@ import { fileURLToPath } from "node:url"; const ROOT = fileURLToPath(new URL("..", import.meta.url)); const STACK_PARENTS = ["stacks", "community", "templates"]; +/** Stacks under these parents are published in the marketplace. */ +const PUBLISHED_PARENTS = ["stacks", "community"]; +const MARKETPLACE = ".claude-plugin/marketplace.json"; +const COMPONENT_FIELDS = ["skills", "commands", "agents", "hooks", "mcpServers"]; const ID = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u; const errors = []; const err = (where, msg) => errors.push(`${where}: ${msg}`); +/** Parsed plugin.json of each stack root that has a valid one. */ +const manifests = new Map(); function dirs(path) { if (!existsSync(path)) return []; @@ -80,6 +96,7 @@ function checkStack(root) { } catch (e) { return err(where, `plugin.json does not parse: ${e.message}`); } + manifests.set(root, manifest); if (manifest.name !== name) err(where, `plugin name "${manifest.name}" != directory "${name}"`); if (!ID.test(name)) err(where, `stack id "${name}" should be lowercase letters, digits and - . _`); if (manifest.icon && !existsSync(join(root, manifest.icon))) err(where, `icon ${manifest.icon} does not exist`); @@ -102,6 +119,85 @@ for (const parent of STACK_PARENTS) for (const root of stacks) checkStack(root); +function checkMarketplace() { + const file = join(ROOT, MARKETPLACE); + if (!existsSync(file)) return err(MARKETPLACE, "missing"); + let catalog; + try { + catalog = JSON.parse(readFileSync(file, "utf8")); + } catch (e) { + return err(MARKETPLACE, `does not parse: ${e.message}`); + } + if (!catalog.name) err(MARKETPLACE, '"name" is required'); + if (!Array.isArray(catalog.plugins)) return err(MARKETPLACE, '"plugins" must be a list'); + const published = new Map( + stacks + .map((root) => [relative(ROOT, root), root]) + .filter(([rel]) => PUBLISHED_PARENTS.includes(rel.split("/")[0])), + ); + const listed = new Set(); + for (const entry of catalog.plugins) { + const where = `${MARKETPLACE} entry "${entry?.name}"`; + const rel = typeof entry?.source === "string" ? entry.source.replace(/^\.\//u, "").replace(/\/$/u, "") : ""; + if (!published.has(rel)) { + err(where, `"source" must be ".//" of an existing stack, got ${JSON.stringify(entry?.source)}`); + continue; + } + if (listed.has(rel)) err(where, `${rel} is listed twice`); + listed.add(rel); + if (entry.name !== rel.split("/").pop()) err(where, `"name" must equal the stack directory name`); + for (const field of COMPONENT_FIELDS) + if (field in entry) err(where, `do not set "${field}"; the stack's plugin.json defines its components`); + const manifest = manifests.get(published.get(rel)); + if (manifest && entry.description !== manifest.description) + err(where, '"description" must equal the description in the stack\'s plugin.json'); + } + for (const rel of published.keys()) if (!listed.has(rel)) err(MARKETPLACE, `${rel} has no entry`); +} + +function git(args) { + const r = spawnSync("git", args, { cwd: ROOT, encoding: "utf8" }); + return r.status === 0 ? r.stdout.trim() : undefined; +} + +function semver(value) { + const m = /^(\d+)\.(\d+)\.(\d+)/u.exec(typeof value === "string" ? value : ""); + return m ? m.slice(1).map(Number) : undefined; +} + +function isHigher(next, previous) { + for (let i = 0; i < 3; i += 1) if (next[i] !== previous[i]) return next[i] > previous[i]; + return false; +} + +function checkVersionBumps() { + const base = process.env.CHECK_BASE; + if (!base) return console.log("note: CHECK_BASE not set, skipping the version bump check."); + const mergeBase = git(["merge-base", base, "HEAD"]); + if (!mergeBase) return err("CHECK_BASE", `no merge base between ${base} and HEAD (fetch full history)`); + for (const root of stacks) { + const rel = relative(ROOT, root); + if (!PUBLISHED_PARENTS.includes(rel.split("/")[0])) continue; + if (!git(["diff", "--name-only", mergeBase, "--", rel])) continue; + const before = git(["show", `${mergeBase}:${rel}/.minimax-plugin/plugin.json`]); + if (before === undefined) continue; // a new stack has nothing to bump from + let previous; + try { + previous = JSON.parse(before).version; + } catch { + continue; + } + const current = manifests.get(root)?.version; + const next = semver(current); + const prev = semver(previous); + if (!next || !prev || !isHigher(next, prev)) + err(rel, `files changed since ${base}, but version ${current} is not higher than ${previous}; bump "version" in .minimax-plugin/plugin.json so marketplace installs see the update`); + } +} + +checkMarketplace(); +checkVersionBumps(); + const upstream = process.env.MINIMAX_CODE_DIR; if (upstream) { const validator = join(upstream, "tools", "plugins", "validate-plugin.mjs"); diff --git a/stacks/README.md b/stacks/README.md index 7cb102f..202cd4f 100644 --- a/stacks/README.md +++ b/stacks/README.md @@ -16,9 +16,14 @@ More team members' stacks are on the way. directory name and the plugin `name`. 2. Copy the template: `cp -R templates/stack-template stacks/`. 3. Edit `.minimax-plugin/plugin.json`, write your skills and `WORKFLOW.md`. -4. Add a row to the table above. +4. Add a row to the table above, and an entry to + [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) + (`"source": "./stacks/"`, the same `description` as `plugin.json`). 5. Run `node scripts/check.mjs` and open a PR. +When you change a stack later, raise its `version` in the same PR; see +[CONTRIBUTING.md](../CONTRIBUTING.md#changing-a-stack). + Keep it public-safe: no internal hostnames, links, project codenames, internal metrics, credentials or customer data. Skills must work for someone outside the team. diff --git a/templates/stack-template/README.md b/templates/stack-template/README.md index 589a7a9..ea69692 100644 --- a/templates/stack-template/README.md +++ b/templates/stack-template/README.md @@ -15,4 +15,6 @@ Then: 4. Rename `skills/example` and write your skills. List every skill in `skills` in `plugin.json`. 5. Write `WORKFLOW.md`. -6. Run `node scripts/check.mjs` from the repository root. +6. Add an entry for the stack to `.claude-plugin/marketplace.json` at the + repository root (see CONTRIBUTING.md). +7. Run `node scripts/check.mjs` from the repository root.