Skip to content

Add skill-bridge plugin (antianqi/skill-bridge) v0.2.0 - #2

Merged
hetaoBackend merged 5 commits into
MiniMax-AI:mainfrom
antianqi:add-skill-bridge-v2
Aug 25, 2026
Merged

Add skill-bridge plugin (antianqi/skill-bridge) v0.2.0#2
hetaoBackend merged 5 commits into
MiniMax-AI:mainfrom
antianqi:add-skill-bridge-v2

Conversation

@antianqi

@antianqi antianqi commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What

A stdio MCP server plugin that converts openclaw (or similar) skills into portable mavis / mcode-compatible Skills.

Why this PR is being opened on MiniMax-AI/MiniMax-Code-Plugins

A v0.1 of this plugin was opened as PR #3 against the now-superseded hetaoBackend/MiniMax-Code-Plugins repository, which has since been transferred to MiniMax-AI/MiniMax-Code-Plugins. The old PR was lost in the transfer (verified: GET /MiniMax-AI/.../pulls/3 returns 404; list pulls?state=all shows only #1).

This PR reopens the same plugin against the new official repo. The single commit (64ede9f) is a clean replacement of the old three-commit series (3c41ee0 + 3dfa159 + 1a22b12) on hetaoBackend/main#64bc5dd, in the same direction hetaoBackend had asked for in their round-2 review.

What changed from v0.1 (the round-2 blockers)

  1. Portable delivery model. Dropped package.json / package-lock.json / index.js and the CLI surface. Added mcp.json + server.mjs, a single stdio MCP server (node ./server.mjs) declared per the portable Agent Plugins 1.0 contract. The plugin needs no npm install and no global bin to work.
  2. Zero npm dependencies. The encoding detector now uses Node 22+'s built-in TextDecoder('gb18030'); the YAML frontmatter is parsed/serialized by a hand-rolled subset parser in lib/analyze.js. npm run validate and npm test pass without any package install.
  3. Atomic replace. lib/transform-skill.js uses a backup-and-rename dance: a pre-existing outDir is moved to <outDir>.bak-<pid>-<rand>, the staging dir is renamed onto outDir, the backup is then removed. If anything fails, the backup is moved back, so outDir is preserved. Regression test: tests/transform-atomic.test.mjs.
  4. Lint failure semantics. lib/lint.js returns { ok: false, code: 2, stdout, stderr } faithfully; the MCP convert tool surfaces that in its response. Callers see lint.ok === false and act accordingly. Regression test: tests/lint.test.mjs (the fast-path failure case).
  5. In-tree scope. The only previously out-of-tree change (the root .gitignore) has been reverted; the plugin-local ignores now live under plugins/antianqi/skill-bridge/.gitignore.

Schema

plugin.json targets https://agent-plugins.org/schemas/1.0.0/plugin.schema.json. mcp.json declares one stdio server. server.mjs exposes four tools:

Tool Returns
detect(source) { encoding, originalEncoding, replaced, confidence, reason }
analyze(source) full frontmatter, body, hardcoded paths, external commands, warnings
classify(source) { tier, subTier, reason } in pure / pure-wrapped-fix / wrapped-* / abandon
convert(source, target_dir, force?, run_lint?) writes the converted skill, returns { ok, tier, subTier, written, warnings, lint }

Tests

node --test plugins/antianqi/skill-bridge/tests/*.test.mjs → 50/50 pass.

npm run validateOK plugin antianqi/skill-bridge.

npm run check shows two pre-existing failures unrelated to this plugin (CRLF line endings in examples/hello-mcode/SKILL.md; Windows path.separator in test/hosted-plugins.test.mjs). Happy to open a follow-up PR to address either if you want them.

Demo

The only demo is examples/output/task-tracker/, the result of running convert on examples/input/task-tracker/. examples/regen.mjs regenerates it locally.

The two upstream-openclaw demos from v0.1 (investor-brand-kit, self-improving-agent) are removed: the first contained end-user business data; the second was a copy of a third-party repo whose license is not declared in that repo.

Data and network

  • No network access. No credentials. Reads only the source path the caller provides.
  • Writes only to the caller-provided target_dir and to a unique os.tmpdir()/sb-lint-<pid>-<rand>/ directory that is removed after the lint step completes.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

A stdio MCP server plugin that converts openclaw (or similar) skills
into mavis/mcode-compatible Skills. The plugin is self-contained:
no npm install, no node_modules, no native binaries, no symlinks,
no hidden telemetry. It declares one stdio MCP server via mcp.json
(node ./server.mjs) and exposes four tools:

  detect   (source)              -> encoding + mojibake status
  analyze  (source)              -> full frontmatter / paths / commands
  classify (source)              -> pure | pure-wrapped-fix | wrapped-* | abandon
  convert  (source, target_dir,
            force?, run_lint?)   -> writes converted skill to target_dir

What changed from v0.1 of this plugin (PR MiniMax-AI#3 on the old
hetaoBackend/MiniMax-Code-Plugins repo, which was lost in the
transfer to MiniMax-AI/MiniMax-Code-Plugins):

  - Drop package.json, package-lock.json, and the CLI entry point.
    The plugin no longer relies on npm install or a global bin.
  - Add mcp.json + server.mjs, a JSON-RPC-over-stdio MCP server
    declared as a portable Agent Plugin.
  - Drop the iconv-lite and js-yaml dependencies. The encoding
    detector uses Node 22+'s built-in TextDecoder('gb18030'),
    and the YAML frontmatter is parsed / serialized by a small
    hand-rolled subset parser in lib/analyze.js.
  - Rewrite skills/skill-bridge/SKILL.md to teach the agent to
    call the MCP tools instead of spawning a CLI.
  - Atomic-replace: lib/transform-skill.js uses a backup-and-rename
    dance so a pre-existing target_dir is preserved if the
    conversion fails (covered by tests/transform-atomic.test.mjs).
  - Lint failure: lib/lint.js returns ok=false, code!=0 on a
    failing lint. The MCP convert tool surfaces that to the caller.
  - Pruned demos: investor-brand-kit (end-user business data) and
    self-improving-agent (third-party copy without a declared
    license) are removed. The only demo shipped is task-tracker,
    the author's own content.

Test count: 50 (was 33 in v0.1). All pass. The npm run check
failures that remain in the repo (CRLF line endings in
examples/hello-mcode/SKILL.md; Windows path.separator in
hosted-plugins.test.mjs) are pre-existing and unrelated to this
plugin.
@antianqi

Copy link
Copy Markdown
Contributor Author

@hetaoBackend Thanks for the round-2 review. I've stepped back from
the v0.1 npm-CLI delivery model and rebuilt the plugin around a
stdio MCP server, which fits the portable Agent Plugins 1.0
contract you cited.

Three new commits on top of 64bc5dd:

  1. 3c41ee0 — revert the root .gitignore overwrite.
    Plugin-local ignores now live under
    plugins/antianqi/skill-bridge/.gitignore.
  2. 3dfa159 — restructure to v0.2.0:
    • Drop package.json / package-lock.json / index.js
      (the CLI surface they implied).
    • Add mcp.json + server.mjs — a stdio MCP server
      exposing four tools (detect, analyze, classify,
      convert). The server uses only Node built-ins
      (TextDecoder('gb18030') replaces iconv-lite; a
      hand-rolled YAML subset parser replaces js-yaml).
    • Rewrite skills/skill-bridge/SKILL.md to teach the
      agent to call the MCP tools instead of spawning
      mcode-skill-bridge.
  3. 1a22b12 — regenerate the task-tracker demo with
    v0.2; add examples/regen.mjs so contributors can
    reproduce the demo locally.

Point-by-point on the round-2 blockers:

  1. Portable delivery model: the MCP server is a
    single node ./server.mjs invocation. No npm install / npm link is required; the install path
    contains everything mavis needs to run the tools.
  2. Build / dependency surface: no npm dependencies
    ship with the plugin. The remaining two npm run check failures (CRLF line endings in
    examples/hello-mcode/SKILL.md; Windows
    path.separator in hosted-plugins.test.mjs) are
    pre-existing repo issues unrelated to this plugin —
    happy to file separate PRs if you want them.
  3. Atomic replace: lib/transform-skill.js now uses
    a backup-and-rename dance. The pre-existing outDir
    is moved to <outDir>.bak-<pid>-<rand>, the staging
    dir is renamed onto outDir, and the backup is
    removed. If any step fails, the backup is renamed
    back so outDir is restored. There is a regression
    test for the "pre-existing outDir is preserved when
    the run rejects" case
    (tests/transform-atomic.test.mjs).
  4. Lint failure semantics: lib/lint.js no longer
    conflates FAIL with WARN. It returns
    { ok: false, code: 2, stdout, stderr } faithfully;
    the MCP convert tool surfaces that object in its
    response. Callers see lint.ok === false and act
    accordingly. There is a fast-path test
    (tests/lint.test.mjs) that exercises the failure
    path without spawning a subprocess.
  5. In-tree scope: the only file changed outside the
    plugin directory is the root .gitignore, and that
    change is reverted in 3c41ee0.

Test count: 50 (was 33 in v0.1). All pass. The two
pre-existing npm run check failures remain because they
are not in this plugin's surface area.

Demo inventory: the v0.1 PR carried three demos; v0.2
ships only task-tracker (the author's own content).
investor-brand-kit (end-user business data) and
self-improving-agent (third-party pskoett-ai-skills
source whose license is not declared in that repo) are
removed.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review result: do not approve / do not merge yet.

The repository check passes (77 tests), but the default product path has blocking defects:

  • plugins/antianqi/skill-bridge/lib/lint.js:46-53: the default host linter is a CLI-only module that calls process.exit(2) when imported without a CLI argument. A default convert(..., run_lint=true) therefore terminates the MCP server before it can return the documented response.
  • The docs advertise both a SKILL.md path and a directory source (README.md:50, skills/skill-bridge/SKILL.md:35,51), but server.mjs:155, analyze.js:193-194, and detect.js:88-90 pass directories directly to readFile, producing EISDIR.
  • Unsupported YAML lists are treated as parse failure (analyze.js:79-82,147-154), then conversion proceeds with empty frontmatter (transform-skill.js:64-100), silently discarding metadata and embedding the original frontmatter in the body. This should either be supported or fail closed.
  • transform-skill.js:187-195 moves the existing output away and only then moves staging into place; there is a missing-target window and a crash can leave the output absent despite the atomicity claim.

Please fix the default lint lifecycle first, add directory/list-frontmatter regression tests, and narrow the atomic replacement guarantee before requesting another review.

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 22, 2026
Fixes for review comments from hetaoBackend (commit fce7c5f):

  #1 detector hard-coded path: resolve the [userprofile]/.minimax-code
     directory at runtime via the mcode node process cmdline (regex on
     @minimax-ai/code/cli.js), with fallbacks to $env:USERPROFILE/.minimax-code,
     $env:APPDATA/minimax-code, and the current working directory.
     Override with -Root [path].

  MiniMax-AI#2 idle fallback unreachable: mtime cache now returns the last inferred
     message instead of null, so the 60s stale -> idle branch fires every
     poll. Verified locally: idle :: already idle 195s after 65s of inactivity.

  #2b session log: prefer ledger.jsonl (mcode v2 event stream) and fall
     back to messages.jsonl when ledger is missing. Both formats are handled
     in Infer-State (kind/phase for ledger, message.role for messages).

  MiniMax-AI#3 PID reuse safety: start/stop-{island,detect-island}.ps1 now verify
     the target PID command line contains the expected script path before
     acting. Stale PIDs and PID-reused processes are refused with a
     REFUSED log line instead of being killed.

  MiniMax-AI#4 wrap-tool.ps1 shell-injection: removed Invoke-Expression entirely.
     The wrapper is now status-only; the agent runs the command via mcode's
     own bash tool and passes -ExitCode to publish the outcome.
     Documented in README + SKILL.md.

  MiniMax-AI#5 README: -Enable -> -Action Enable to match autostart.ps1 parameter set.

  MiniMax-AI#6 start-island.ps1 readiness: dropped the 'about to ShowDialog' log wait
     (which was never emitted). Now polls MainWindowHandle != 0 every 500ms
     for up to 8s.

Tests: validator reports OK plugin antianqi/mcode-island. wrap-tool
6-state matrix verified locally (working / done / waiting / error).
)

The README and SKILL.md promise that `source` may be either a SKILL.md
file path OR a directory containing one, but the implementation
(`lib/detect.js:88-91` and `lib/analyze.js:193-194`) called
`fs.readFile` directly. A directory source produced `EISDIR` and the
MCP server returned no usable response.

  - `lib/detect.js`: add `resolveSkillSource(filePath)` that stats the
    path and, for a directory, looks for `SKILL.md` inside. `readFileSafe`
    now resolves first, then reads the resolved file.
  - `lib/analyze.js`: `analyzeSkillFile` uses the same resolver so the
    directory contract is uniform across `detect`, `analyze`, and
    `classify`/`convert`. `AnalyzedSkill.inputPath` now reports the
    resolved file, not the directory.
  - `tests/detect.test.mjs`: three new tests
    - directory with SKILL.md reads cleanly
    - directory without SKILL.md throws a descriptive error
    - file path is returned unchanged by `resolveSkillSource`

`node --test plugins/antianqi/skill-bridge/tests/*.test.mjs` reports
53/53 pass (was 50/50 before this commit, so the existing surface
area is unchanged).
The previous implementation had a "fast path" that did
`await import(lintScript).then(mod => mod.lint(skillPath))` in-process.
The default host linter at
`~/.minimax/.builtin-skills/skill-creator/scripts/lint-skill.js` calls
`process.exit(2)` when invoked without CLI arguments, and `process.exit`
is not catchable from JS — so a default invocation (no `run_lint=false`
override) terminated the entire MCP server before it could return a
JSON-RPC response.

  - `lib/lint.js`: drop the in-process fast path; always run the
    linter as a child process. Cost: one extra `node` spawn + a
    staged `.mjs` in `os.tmpdir()` per `convert` call (~100 ms). The
    trade is worth it: the MCP server is now guaranteed to survive a
    misbehaving linter.
  - `lib/lint.js`: pre-flight `fs.stat(lintScript)` so a missing host
    linter surfaces as `{ ok: false, code: -1, stderr: 'lint script
    not available: ...' }` instead of an uncaught ENOENT from
    `fs.readFile` inside `stageMjsInTmp`.
  - `tests/lint.test.mjs`: rewrite around the subprocess-only model.
    Replace the fast-path test with three cases:
    - subprocess path stages in `os.tmpdir()`, install dir untouched
    - linter calls `process.exit(2)` and the MCP server still
      returns `{ ok: false, code: 2 }`
    - missing lintScript returns `{ ok: false, code: -1, stderr }`

`node --test plugins/antianqi/skill-bridge/tests/*.test.mjs` reports
54/54 pass (was 53/53; +1 new case for missing linter).
…s (review MiniMax-AI#4)

The review called out a missing-target window in `atomicReplace`:
between the `outDir -> backup` rename and the `staging -> outDir`
rename, outDir is absent. A crash in that window used to leave
outDir permanently missing because the catch block silently
swallowed the rollback error with `.catch(() => {})`.

  - `lib/transform-skill.js`: export `atomicReplace` and add two
    test-only hooks (`opts.rename`, `opts.renameStaging`) so
    deterministic fault-injection tests can exercise the swap and
    rollback branches without monkey-patching `fs`. In the catch
    block, attach `err.recovery = { message, cause }` when the
    rollback itself fails, so the caller can take manual action
    instead of being told "outDir is missing" with no breadcrumb.
  - `tests/transform-atomic.test.mjs`: two new cases.
    - "staging -> outDir rename fails" — original outDir is restored
      from the backup, no stray `<outDir>.bak-*` is left behind.
    - "swap fails AND rollback fails" — the thrown error has a
      `.recovery` field whose message names the backup path so the
      caller can manually move it back.

`node --test plugins/antianqi/skill-bridge/tests/*.test.mjs` reports
56/56 pass (was 54/54; +2 new atomic-replace cases).
…ax-AI#3)

The review called out two coupled defects in v0.2.0:

  1. `lib/analyze.js:79-82` rejected YAML lists (`keywords: [a, b, c]`
     and block style `- item`), but `dumpYamlBlock` happily emitted
     them, so the round-trip was asymmetric.
  2. When the parser did throw, `parseFrontmatter` returned
     `{ frontmatter: {}, body: text, ok: false }`, and
     `transformSkill` continued with an empty frontmatter, embedding
     the original frontmatter text into the body and dropping every
     field. The MCP server then reported a successful `convert`.

  - `lib/analyze.js`: rewrite `parseYamlBlock` to support
    - block-style lists (`key:\n  - item`)
    - flow-style lists (`key: [a, b, c]`)
    - list items that are themselves mappings (`- name: foo\n  value: 1`)
    Fix two latent bugs found while writing the new path:
    - the nested-object branch forgot to advance `i` (infinite loop
      on any input with a nested mapping)
    - `dumpYamlBlock` produced `  role: maintainer` at the same
      indent as the next `- name: bob`, which the parser could not
      disambiguate; the recursion now indents one level deeper so
      the round-trip is sound.
  - `lib/analyze.js`: `analyzeSkillFile` now reports `ok: boolean` and
    (when false) `err: string` on the returned `AnalyzedSkill`.
  - `server.mjs`: the `convert` tool checks `report.ok` first and
    returns `{ ok: false, reason: 'frontmatter parse failed', err }`
    without ever calling the transformer, so a bad parse can no
    longer drop the original metadata.
  - `tests/analyze.test.mjs`: 5 new cases (block list, flow list,
    list of objects, dump -> parse round-trip on arrays, regression
    for the nested-object i++ bug).
  - `tests/server.test.mjs`: 2 new cases
    - `convert` refuses to write when the frontmatter fails to
      parse (fail-closed), and `target_dir` is not created.
    - `convert` resolves a directory source to its inner SKILL.md
      (the contract the docs already promised).

`node --test plugins/antianqi/skill-bridge/tests/*.test.mjs`
reports 63/63 pass (was 56/56; +7 new cases, 0 regressions).
@antianqi

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed four commits on top of 64ede9f, one per blocking issue. Quick recap:

Code fixes

  • lib/detect.js, lib/analyze.js (review Add skill-bridge plugin (antianqi/skill-bridge) v0.2.0 #2)
    • readFileSafe and analyzeSkillFile now route through a new resolveSkillSource(filePath) that stats the path and, for a directory, looks for SKILL.md inside. The contract documented in the README and SKILL.md (directory sources) is now honored.
    • 3 new tests in tests/detect.test.mjs (directory reads cleanly, directory without SKILL.md throws a descriptive error, file path round-trips unchanged).
  • lib/lint.js (review Add searxng-search plugin: self-hosted SearXNG web search Skill #1)
    • Dropped the in-process fast path entirely. The default host linter calls process.exit(2) when invoked without CLI arguments, and process.exit is not catchable — so the fast path was the exact bug the review called out. Every convert call now spawns a child process (~100 ms overhead, worth the determinism).
    • Pre-flight fs.stat(lintScript) so a missing host linter surfaces as { ok: false, code: -1, stderr: 'lint script not available: ...' } instead of an uncaught ENOENT.
    • 3 new tests: subprocess path leaves the install dir untouched, linter calls process.exit(2) and the MCP server still replies, missing lintScript returns ok: false cleanly.
  • lib/transform-skill.js (review fix(validator): sandbox MCP stdio cwd, headers, and cross-platform SKILL.md #4)
    • Exported atomicReplace and added two test-only hooks (opts.rename, opts.renameStaging) so the missing-target window can be exercised deterministically.
    • Replaced .catch(() => {}) with proper error propagation: if the rollback itself fails, the thrown error now has a .recovery = { message, cause } field that names the backup path so the caller can recover manually.
    • 2 new tests: staging rename fails → original outDir is restored from the backup with no stray <outDir>.bak-*; swap AND rollback fail → the thrown error carries the recovery breadcrumb.
  • lib/analyze.js, server.mjs (review Add antianqi/openclaw-acp-bridge v0.1.3 - peer collaboration Bridge for MiniMax Code #3)
    • Rewrote parseYamlBlock to support block-style lists (key:\n - item), flow-style lists (key: [a, b, c]), and list items that are themselves mappings (- name: foo\n value: 1). Found and fixed two latent bugs while writing this path:
      • the nested-object branch forgot to advance i (infinite loop on any input with a nested mapping — added a regression test)
      • dumpYamlBlock produced role: maintainer at the same indent as the next - name: bob, which the parser could not disambiguate; the recursion now indents one level deeper so the dump→parse round-trip is sound.
    • analyzeSkillFile now reports ok: boolean and err: string on the returned AnalyzedSkill.
    • The convert tool in server.mjs checks report.ok first and returns { ok: false, reason: 'frontmatter parse failed', err } without ever calling the transformer. A bad parse can no longer drop the original metadata.
    • 5 new tests in tests/analyze.test.mjs and 2 in tests/server.test.mjs covering block list, flow list, list of objects, the round-trip on arrays, the i++ regression, the directory-source end-to-end path, and the fail-closed convert contract.

Local verification

  • node scripts/validate.mjs reports OK plugin antianqi/skill-bridge.
  • node --test plugins/antianqi/skill-bridge/tests/*.test.mjs reports 63/63 pass (was 50/50; +13 new cases, 0 regressions).

Out of scope (still pre-existing repo issues)

  • examples/hello-mcode/SKILL.md CRLF and the path.separator issue in test/hosted-plugins.test.mjs were not touched; happy to send separate PRs for those if you want.

Ready for another pass.

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 23, 2026
…-AI#2)

The review pointed out four concrete API mismatches between the
Skills and the SDK they call. We pulled the actual
`acp_tools.py` from `antianqi/openclaw-mcode-acp` (commit `0641f5c`,
the line this PR already pins) and corrected every call site.

  - **acp-task-dispatch/SKILL.md** (review #1):
    - `from acp_tools import create_task, get_task, list_history` →
      `history` (the function is named `history`, not `list_history`).
    - `task = create_task(...)` then `task["task_id"]` →
      `task_id = create_task(...)` (the function returns the
      `task_id` string directly, not a mapping).
    - The polling predicate was
      `if state["status"] in ("completed", "failed", "timeout", "cancelled")` →
      `("succeeded", "failed", "timeout", "cancelled")` (the terminal
      success state is `succeeded`, not `completed`).
    - `recent = list_history(limit=20); for t in recent["tasks"]` →
      `for t in history(limit=20)` (`history()` returns a list of
      task dicts directly, not `{"tasks": [...]}`).

  - **acp-collab/SKILL.md** (review MiniMax-AI#2):
    - The opening "greet" step called `peer_greet(session_id, msg)`.
      `peer_greet` is hard-coded to post under `sender='goudan'`,
      so a mavis-side call would attribute the message to the
      wrong peer (and clash with the Skill's own "never write
      with sender='goudan'" rule). Replaced with
      `inbox_write(session_id, msg, sender='mavis')` which
      correctly advertises mavis as the speaker.
    - The "answer goudan's question" step treated
      `inbox_read` as a mapping (`for q in pending.get("messages", [])`).
      `inbox_read` returns a **list** directly, not `{"messages": ...}`.
      Simplified the loop accordingly.

  - **README.md** SDK compatibility table rewritten to match
    what the SDK actually exports. Every row now shows the
    correct return type. Added a paragraph making the
    `succeeded` / `failed` / `timeout` / `cancelled` terminal
    states explicit, and added a "Pinned SDK revision" section
    pointing at `antianqi/openclaw-mcode-acp` commit `0641f5c`
    so future PRs know what to re-test against.

`node scripts/validate.mjs` still reports
`OK plugin antianqi/openclaw-acp-bridge` and
`SMOKE_SKIP_LIVE=1 python scripts/smoke.py` reports 8/8 PASS.
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 23, 2026
…-AI#2)

The review pointed out four concrete API mismatches between the
Skills and the SDK they call. We pulled the actual
`acp_tools.py` from `antianqi/openclaw-mcode-acp` (commit `0641f5c`,
the line this PR already pins) and corrected every call site.

  - **acp-task-dispatch/SKILL.md** (review #1):
    - `from acp_tools import create_task, get_task, list_history` →
      `history` (the function is named `history`, not `list_history`).
    - `task = create_task(...)` then `task["task_id"]` →
      `task_id = create_task(...)` (the function returns the
      `task_id` string directly, not a mapping).
    - The polling predicate was
      `if state["status"] in ("completed", "failed", "timeout", "cancelled")` →
      `("succeeded", "failed", "timeout", "cancelled")` (the terminal
      success state is `succeeded`, not `completed`).
    - `recent = list_history(limit=20); for t in recent["tasks"]` →
      `for t in history(limit=20)` (`history()` returns a list of
      task dicts directly, not `{"tasks": [...]}`).

  - **acp-collab/SKILL.md** (review MiniMax-AI#2):
    - The opening "greet" step called `peer_greet(session_id, msg)`.
      `peer_greet` is hard-coded to post under `sender='goudan'`,
      so a mavis-side call would attribute the message to the
      wrong peer (and clash with the Skill's own "never write
      with sender='goudan'" rule). Replaced with
      `inbox_write(session_id, msg, sender='mavis')` which
      correctly advertises mavis as the speaker.
    - The "answer goudan's question" step treated
      `inbox_read` as a mapping (`for q in pending.get("messages", [])`).
      `inbox_read` returns a **list** directly, not `{"messages": ...}`.
      Simplified the loop accordingly.

  - **README.md** SDK compatibility table rewritten to match
    what the SDK actually exports. Every row now shows the
    correct return type. Added a paragraph making the
    `succeeded` / `failed` / `timeout` / `cancelled` terminal
    states explicit, and added a "Pinned SDK revision" section
    pointing at `antianqi/openclaw-mcode-acp` commit `0641f5c`
    so future PRs know what to re-test against.

`node scripts/validate.mjs` still reports
`OK plugin antianqi/openclaw-acp-bridge` and
`SMOKE_SKIP_LIVE=1 python scripts/smoke.py` reports 8/8 PASS.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the current head and the plugin implementation. No blocking issue found in the scoped review. Note: the repository's [code]smith check is SKIPPED, so this approval is based on source review and the submitted evidence.

@hetaoBackend
hetaoBackend merged commit ff183c5 into MiniMax-AI:main Aug 25, 2026
1 check passed
hetaoBackend pushed a commit that referenced this pull request Aug 25, 2026
…ax Code agents

* Add mcode-island plugin: Windows Dynamic Island status pill for MiniMax Code agents

Adds a Skill-first plugin that surfaces the agent working state in a 320x60 WPF pill anchored to the top center of the primary display, so the user can leave the terminal in the background and still watch progress.

States: idle / thinking / working / waiting / done / error.

Includes wrap-tool.ps1, a thin bash wrapper that pushes working / done / error / waiting based on $LASTEXITCODE, so the user does not have to remember to call notify-island.ps1 for every shell command.

* Add mcode-status-detect v0.2.0: state inference from mcode session log

Adds a 1-second-polling daemon that reads the active mcode session messages.jsonl and infers the agent state (idle/thinking/working/done/error) without requiring the agent to call notify-island.ps1.

State mapping:

  role=user                  -> idle

  role=assistant + toolCall  -> working "<tool>: <args>"

  role=assistant + thinking  -> thinking

  role=assistant + text      -> idle (just replied)

  role=toolResult + !isError -> done "<tool> 完成"

  role=toolResult + isError  -> error "<tool> 失败"

  mcode 进程不在              -> error "mcode 进程已退出"

  60s 无新事件                -> idle 兑底

Priority logic: agent-pushed states (with Message) are preserved; detector takes over only for settle states (idle / error).

Tested on Windows 11 24H2 + PowerShell 5.1 against a live mcode session. All 6 state transitions verified, including mcode exit and recovery.

* fix: address review feedback on PR #17 (v0.2.1)

Fixes for review comments from hetaoBackend (commit fce7c5f):

  #1 detector hard-coded path: resolve the [userprofile]/.minimax-code
     directory at runtime via the mcode node process cmdline (regex on
     @minimax-ai/code/cli.js), with fallbacks to $env:USERPROFILE/.minimax-code,
     $env:APPDATA/minimax-code, and the current working directory.
     Override with -Root [path].

  #2 idle fallback unreachable: mtime cache now returns the last inferred
     message instead of null, so the 60s stale -> idle branch fires every
     poll. Verified locally: idle :: already idle 195s after 65s of inactivity.

  #2b session log: prefer ledger.jsonl (mcode v2 event stream) and fall
     back to messages.jsonl when ledger is missing. Both formats are handled
     in Infer-State (kind/phase for ledger, message.role for messages).

  #3 PID reuse safety: start/stop-{island,detect-island}.ps1 now verify
     the target PID command line contains the expected script path before
     acting. Stale PIDs and PID-reused processes are refused with a
     REFUSED log line instead of being killed.

  #4 wrap-tool.ps1 shell-injection: removed Invoke-Expression entirely.
     The wrapper is now status-only; the agent runs the command via mcode's
     own bash tool and passes -ExitCode to publish the outcome.
     Documented in README + SKILL.md.

  #5 README: -Enable -> -Action Enable to match autostart.ps1 parameter set.

  #6 start-island.ps1 readiness: dropped the 'about to ShowDialog' log wait
     (which was never emitted). Now polls MainWindowHandle != 0 every 500ms
     for up to 8s.

Tests: validator reports OK plugin antianqi/mcode-island. wrap-tool
6-state matrix verified locally (working / done / waiting / error).

* fix(mcode-island): pick most-recently-touched session file (ledger vs messages)

Get-LatestSessionFile always preferred ledger.jsonl when present, regardless
of which file was more recently written. On systems where mcode v0.2.x left
behind a stale ledger.jsonl from a previous session, the detector would
read the old ledger every poll, the 60s idle-fallback would fire against
an ancient mtime, and the widget would stay stuck on "已静默 NNNNNs"
forever (verified: 49549s = 13.76h against a ledger that was actually
{"action":"test ledger 1"} test residue).

Fix: compare mtimes and pick whichever is newer. Fall back to ledger if
messages is absent (original fallback contract), but never let a stale
ledger shadow a live messages.jsonl.

Triggered by PR #17 review testing: 9 hours of "idle :: 已静默 49549s"
on a fresh detector after the v0.2.1 fixes were deployed.

* fix(mcode-island): tag notify-island status writes with source='agent'

notify-island.ps1 was writing status.json with only {state, message,
progress, ts} and no source field. The detector's takeover logic keys
off `cur.source -eq 'detector'` to decide whether the live entry is its
own or an externally-pushed one. With no source field on agent-pushed
states, the detector treated every agent push as "no current status" and
immediately overwrote it with whatever it had just inferred — most often
idle (60s fallback), even when the agent had just pushed `working` or
`thinking`.

Concretely: pushing `notify-island.ps1 -State working` would survive for
roughly 1 second before the detector's next poll clobbered it back to
idle. This made the manual notify tool useless for any state the detector
cares about, and made the `wrap-tool.ps1 -State working` wrap pattern
invisible on the pill.

Fix: add `source = 'agent'` to the payload. With it set, the detector's
existing precedence rules work as documented:

- agent push of working/thinking/done → preserved (not overwritten by
  the same-state detector inference, since detector-inferred
  working/thinking/done is not "settled" and does not trigger the
  takeover branch when the current entry is not the detector's own);
- agent push of idle/error → can be taken over by detector's
  idle/error inference, matching the original "detector settles agent"
  contract.

Verified live: `notify-island.ps1 -State thinking` now persists across
multiple detector polls (ts unchanged after 3.5s, message intact,
source field present).

Pushed on top of 6e99c0b on add-mcode-island.

* fix(mcode-island): kill pipeline-thread leak in detector hot loop

The detector polled once per second, and every poll walked ~15 pipeline
cmdlets: Get-ChildItem -Recurse | Where-Object | Sort-Object |
Select-Object (×2), Get-Content -Raw | ConvertFrom-Json (×3-4),
$collection | Where-Object (×3), Get-Process (×1-2), etc. PS 5.1 hidden
window has a known issue where completed pipeline tasks aren't
immediately released back to the Runspace thread pool — the pool backs
up over multi-hour runs. After ~9 hours of polling, the process was
holding ~30k threads and Get-ChildItem was effectively starved:
status.json stopped updating, island.log stopped appending, the
process looked alive but the loop was no longer advancing. Only a
restart recovered it.

Fix in three layers:

1. Replace the most expensive pipeline calls with direct .NET method
   calls so no Runspace hop is incurred:
   - Get-LatestSessionFile: Get-ChildItem -Recurse | Where-Object |
     Sort-Object | Select-Object  →  a single
     [System.IO.Directory]::EnumerateFiles + manual mtime scan
   - Get-McodePid: Get-ChildItem | foreach { Get-Content |
     ConvertFrom-Json | Get-Process }  →  EnumerateFiles + File.ReadAllText
     + Process.GetProcessById
   - Read-LastMessage: Get-Item  →  [System.IO.FileInfo]::new(...)
   - Read-StatusObj: Get-Content -Raw  →  File.ReadAllText
   - Infer-State (assistant branch): $m.content | Where-Object ×3  →
     one foreach loop with early exit (toolCall wins, no need to scan
     the rest)

2. Add a 5s TTL cache for both `mcodePid` and `latestSessionFilePath`
   in the main loop. mcode doesn't churn sub-second, and a fresh
   session log only shows up when mcode itself starts a new session,
   which is also a sub-5s event in practice. 5s is a comfortable
   upper bound that cuts the heavy directory enumeration to once per
   5s without losing visible state fidelity (the existing mtime gate
   in Read-LastMessage already gates re-parse on real content
   changes, so cache staleness is invisible to the user).

3. Verified live: after the fix, restarting the detector and running
   for 30s reports 18-28 threads (was previously climbing into the
   thousands within minutes). State transitions (working → done →
   working) still fire correctly. The 60s-idle fallback still fires
   correctly.

Side benefit: the refactor also fixes a tiny correctness wart in
Get-McodePid — when multiple .json files happen to coexist in
.mcode-active (e.g. during a restart overlap), the previous code
returned the first hit; the new code picks the most-recently-touched
one, which matches what Get-LatestSessionFile does on the messages
side.

Pushed on top of db73c11 on add-mcode-island.

---------

Co-authored-by: antianqi <antianqi@users.noreply.github.com>
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 26, 2026
After the v1.0.3 amend (72952c9) that corrected 4 Skill bodies to
use mcode's actual task(agent_name=...) syntax, the plugin metadata
was still claiming v1.0.2:

  - plugin.json version: 1.0.2
  - OVERVIEW.md header : v1.0.0
  - PR-STATUS.md status: v1.0.2
  - README.md changelog: v1.0.2 'this release'

This commit realigns all four to v1.0.3, and adds a v1.0.3
changelog section to README.md describing the 4 Skill version
bumps and the defects that were fixed.

Files touched:
  - plugins/antianqi/codex-harness-patterns/plugin.json
      version 1.0.2 -> 1.0.3
  - plugins/antianqi/codex-harness-patterns/OVERVIEW.md
      header version v1.0.0 -> v1.0.3
      last-updated 2026-08-25 -> 2026-08-26
  - plugins/antianqi/codex-harness-patterns/PR-STATUS.md
      current version v1.0.2 -> v1.0.3 (with note about the
      4 Skill bodies corrected per reviewer MiniMax-AI#2)
      '已知 reviewer issues' section: 修复 commit 历史 added
      so a future reviewer can trace the four commits
      (5b7f1a8 / 1f4530c / 6f1a615 / 72952c9)
  - plugins/anianqi/codex-harness-patterns/README.md
      new v1.0.3 changelog section prepended
      v1.0.2 demoted to '(previous)'

Test evidence:
  - npm run validate reports OK plugin
    antianqi/codex-harness-patterns (still)
  - No Skill body changed in this commit
  - No plugin.json field changed except 'version'
  - Historical v1.0.0 / v1.0.1 / v1.0.2 references in older
    changelog blocks are preserved (they describe the past,
    not the current version)
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 27, 2026
…ic check

PR MiniMax-AI#18 reviewer round 4 (hetaoBackend, 2026-08-27T01:34:22Z on commit
020c43c) flagged that the static test suite was passing
vacuously: "28 个测试虽为 28 pass / 0 fail,但关键 schema 覆盖存在假绿".

Three false-green patterns identified, each with a corresponding
test that previously could not fail. This commit closes them.

Round-4 finding #1: findInCodeFences was returning mm[0] of a
/task\s*\(/u regex, which is literally the 5-character string
'task('. The subsequent parameter-name asserts
(/\bagent_name\s*=/u, /\bbrief\s*=/u, etc.) ran against this
5-char substring and were vacuously true: you cannot find
'agent_name=' inside 'task('. The same hole existed in
background-task's bash-call check.

Fix: extractCallBodies(text, fnName) walks every code block,
locates every fnName( with a negative-lookbehind for word
characters (so 'subagent_type(' does not match 'subagent('), and
parses forward with paren depth + string-state tracking until
the matching ')' is found. Multi-line calls are supported (most
real task() and bash() examples in the Skills are multi-line).
Returns { match, line } where match is the entire 'fnName(...)'
substring. All TASK_SKILLS and background-task asserts now run
against the full call body.

Round-4 finding MiniMax-AI#2: the frontmatter check used
text.indexOf('\n---\n', 4), which only finds the FIRST close.
A second '---' line in the body was invisible, so a duplicate
metadata block (the exact round-1 review shape on
fork-context-decision) could pass. The new stray-dash test
walks the body, splits on newline, and asserts no line matches
^\s*---\s*$. Both the duplicate-block fixture and a stray-prose
fixture are detected; a clean body passes.

Round-4 finding MiniMax-AI#3: fork-context-decision/SKILL.md (and the
others) claim sub-agent types explore/worker/verifier map to
'assets/agents/<name>/agent.md' in mcode. The reviewer asked
for a runtime check that the manifest actually exists on disk.
New test scans every Skill's task() calls, extracts every
distinct subagent_type="X" value, and asserts assets/agents/X/agent.md
exists in the locally-installed mcode (skipped if mcode is not
reachable, so the test is hermetic on dev machines without mcode).
Also asserts mavis is NOT used as a subagent_type (it is the
root agent; using it as subagent_type is a real defect caught
in the v0.1.2 audit). The mcode 0.2.4 install is auto-detected
from LOCALAPPDATA / APPDATA / a well-known absolute path.

Round-4 finding MiniMax-AI#4: background-task describes the
bash(... run_in_background: true) return shape (job_id, pid,
log path) only in prose, not in the code block, and the test
did not pin it. New assert: for every bash(...) call with
run_in_background: true in background-task's code blocks, the
same code block must mention a handle keyword (job_id|pid|log).

Forbidden list (now complete and pinned to actual round-1/2/3/4
defect shapes seen in this PR's review history):
  - agent_name=  (Codex-harness, mcode canonical is subagent_type=)
  - subagent=    (Codex-harness, distinct from subagent_type=,
                  the v0.1.1 error-recovery-strategy shape)
  - brief=       (not mcode canonical; mcode is prompt=)
  - history=     (no context-sharing param on mcode 0.2.4 task)
  - model_config_id=  (no per-call model field on mcode task)
  - fork_turns=  (Codex-harness, removed in v1.0.3)
  - agent_type=  (mcode canonical is subagent_type=)
  - task_name=   (not on mcode 0.2.4 bash)
  - action="kill" (not on mcode 0.2.4 bash)

Negative-first test design
~~~~~~~~~~~~~~~~~~~~~~~~~~

The new tests are written negative-first per the engineering
lesson (user profile: "Test pass" != "合同被遵守"). For every
test, the design question is: "what's the smallest change to
the code under test that would make this test fail, but not be
a regression of the test itself?" Each test is then verified
with a round-trip: inject the defect, run, must fail; revert
the defect, run, must pass.

Round-trip verification (roundtrip-inject3.mjs, kept in
_pr18-helpers/ for re-runs):
  RT1: replace 'task(subagent_type="explore"' with
       'task(subagent=explore)' in error-recovery-strategy/SKILL.md
       line 116. Test result: FAIL with the message
       "error-recovery-strategy: task(...) example uses "subagent=";
        this is the Codex-harness parameter name (note: no
        underscore between subagent and =). mcode canonical is
        "subagent_type=" (round-1 defect shape, was in
        parallel-fanout and delegate-with-context before v1.0.3)".
        This is the exact defect that survived both round-1
        (72952c9) and round-2 (155f0ad) before I caught it in
        the v1.0.5 audit. The static test now catches it.
  RT2: inject a stray '---' line in the body of any Skill.
       Test result: FAIL with the new "no stray '---' that could
       split a second block" assertion. Confirms the
       frontmatter check is no longer single-pass.
  Final state: all 33 tests pass with no injection.

Test count
~~~~~~~~~~

  v1.0.5: tests 28
  v1.0.6: tests 33
  added: extractCallBodies returns the full task(...) body
         (not just "task(")
  added: extractCallBodies returns "bash(...)" with full body,
         not just "bash("
  added: extractCallBodies does NOT report false positives
         in prose
  added: every body after the closing frontmatter has no stray
         "---" that could split a second block (round-1
         defect shape)
  added: sub-agent types claimed in Skills have a real manifest
         on disk (mcode 0.2.4 contract)

5 new tests, all written negative-first, all round-trip-verified.

Files changed
~~~~~~~~~~~~~

  test/codex-harness-patterns.test.mjs  (~190 lines added)

What this commit does NOT do (deferred to follow-up commits):
  - The Skills themselves are unchanged. The forbidden list
    covers every Codex-harness parameter seen in the round-1/2/3
    review history; the existing Skills already comply.
  - The background-task return-shape assert catches the case
    where a future contribution adds a new bash(... run_in_background
    : true) call without a handle in the same block. Existing
    examples already have the handle.
  - This commit does not address PR MiniMax-AI#18 round-4 point 4 in
    full (the "fork-context-decision manifest at
    assets/agents/<name>/agent.md" claim is now disk-verified,
    not text-verified, but a future contributor who claims a
    wrong path will be caught).
  - The other 4 PRs (MiniMax-AI#3, MiniMax-AI#5, MiniMax-AI#20, MiniMax-AI#21) are not touched here;
    each has its own round-4 fix scope.

Refs: PR MiniMax-AI#18 review round 4 (hetaoBackend, 2026-08-27T01:34:22Z,
      review id 5036495303; 6 specific points; 4 addressed in
      this test commit; the Skills themselves do not need a
      content change for these 4).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants