Add skill-bridge plugin (antianqi/skill-bridge) v0.2.0 - #2
Conversation
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.
|
hetaoBackend
left a comment
There was a problem hiding this comment.
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 callsprocess.exit(2)when imported without a CLI argument. A defaultconvert(..., 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), butserver.mjs:155,analyze.js:193-194, anddetect.js:88-90pass directories directly toreadFile, producingEISDIR. - 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-195moves 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.
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).
|
Thanks for the review. Pushed four commits on top of Code fixes
Local verification
Out of scope (still pre-existing repo issues)
Ready for another pass. |
…-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.
…-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
left a comment
There was a problem hiding this comment.
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.
…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>
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)
…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).
What
A stdio MCP server plugin that converts
openclaw(or similar) skills into portablemavis/mcode-compatible Skills.Why this PR is being opened on
MiniMax-AI/MiniMax-Code-PluginsA v0.1 of this plugin was opened as PR #3 against the now-superseded
hetaoBackend/MiniMax-Code-Pluginsrepository, which has since been transferred toMiniMax-AI/MiniMax-Code-Plugins. The old PR was lost in the transfer (verified:GET /MiniMax-AI/.../pulls/3returns 404;list pulls?state=allshows 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) onhetaoBackend/main#64bc5dd, in the same direction hetaoBackend had asked for in their round-2 review.What changed from v0.1 (the round-2 blockers)
package.json/package-lock.json/index.jsand the CLI surface. Addedmcp.json+server.mjs, a single stdio MCP server (node ./server.mjs) declared per the portable Agent Plugins 1.0 contract. The plugin needs nonpm installand no global bin to work.TextDecoder('gb18030'); the YAML frontmatter is parsed/serialized by a hand-rolled subset parser inlib/analyze.js.npm run validateandnpm testpass without any package install.lib/transform-skill.jsuses a backup-and-rename dance: a pre-existingoutDiris moved to<outDir>.bak-<pid>-<rand>, the staging dir is renamed ontooutDir, the backup is then removed. If anything fails, the backup is moved back, sooutDiris preserved. Regression test:tests/transform-atomic.test.mjs.lib/lint.jsreturns{ ok: false, code: 2, stdout, stderr }faithfully; the MCPconverttool surfaces that in its response. Callers seelint.ok === falseand act accordingly. Regression test:tests/lint.test.mjs(the fast-path failure case)..gitignore) has been reverted; the plugin-local ignores now live underplugins/antianqi/skill-bridge/.gitignore.Schema
plugin.jsontargetshttps://agent-plugins.org/schemas/1.0.0/plugin.schema.json.mcp.jsondeclares one stdio server.server.mjsexposes four tools:detect(source){ encoding, originalEncoding, replaced, confidence, reason }analyze(source)classify(source){ tier, subTier, reason }inpure/pure-wrapped-fix/wrapped-*/abandonconvert(source, target_dir, force?, run_lint?){ ok, tier, subTier, written, warnings, lint }Tests
node --test plugins/antianqi/skill-bridge/tests/*.test.mjs→ 50/50 pass.npm run validate→OK plugin antianqi/skill-bridge.npm run checkshows two pre-existing failures unrelated to this plugin (CRLF line endings inexamples/hello-mcode/SKILL.md; Windowspath.separatorintest/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 runningconvertonexamples/input/task-tracker/.examples/regen.mjsregenerates it locally.The two upstream-
openclawdemos 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
target_dirand to a uniqueos.tmpdir()/sb-lint-<pid>-<rand>/directory that is removed after the lint step completes.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.