Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -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)"
}
}
]
}
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
17 changes: 15 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
2. **Copy the template** to `community/<id>/`. 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/<id>"` 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
Expand All @@ -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:
Expand Down
9 changes: 7 additions & 2 deletions CONTRIBUTING_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

1. **先开一个「New stack」issue**,用几句话说清楚你是谁、怎么用 agent、打算放哪几样东西。
2. **从模板复制**到 `community/<id>/`。`id` 只用小写字母、数字和连字符,并且要和 `.minimax-plugin/plugin.json` 里的 `name` 一致。
3. **本地跑校验**,把输出贴进 PR:
3. **登记到插件市场**:在 `.claude-plugin/marketplace.json` 里加一条,`"name"` 填 id,`"source"` 填 `"./community/<id>"`,`description` 和 `plugin.json` 里的保持一致。不要写 `skills` 等组件字段,这些由 `plugin.json` 定义。
4. **本地跑校验**,把输出贴进 PR:

```bash
node scripts/check.mjs
Expand All @@ -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` 跑同样的检查。

## 怎么审

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

<details>
<summary>More install options</summary>

Expand Down
11 changes: 11 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

<details>
<summary>更多安装方式</summary>

Expand Down
100 changes: 98 additions & 2 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 [];
Expand Down Expand Up @@ -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`);
Expand All @@ -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 "./<stacks|community>/<id>" 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");
Expand Down
7 changes: 6 additions & 1 deletion stacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>`.
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/<id>"`, 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.
Expand Down
4 changes: 3 additions & 1 deletion templates/stack-template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading