Skip to content

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

Merged
hetaoBackend merged 6 commits into
MiniMax-AI:mainfrom
antianqi:add-mcode-island
Aug 25, 2026
Merged

Add mcode-island plugin: Windows Dynamic Island status pill for MiniMax Code agents#17
hetaoBackend merged 6 commits into
MiniMax-AI:mainfrom
antianqi:add-mcode-island

Conversation

@antianqi

@antianqi antianqi commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

mcode-island — Windows Dynamic Island for MiniMax Code agents

A Skill-first Plugin that surfaces the agent's working state in a small WPF pill
anchored to the top center of the primary display, so the user can leave the
terminal in the background and still see exactly what the agent is doing.

idle thinking working waiting done error

The agent's only contract with the widget is: write JSON to
%APPDATA%\mcode-island\status.json (or call the notify-island.ps1 helper
that does that for you). The widget polls that file every 400 ms.

The problem this solves

While the agent runs a long tool call (compile, install, test, refactor), the
user often switches away from the terminal to read code, check docs, or browse
the web. There is no visible progress signal. The agent may also be paused on
a permission prompt, or have failed silently. mcode-island makes all of
that visible at a glance, without forcing the user to switch back.

Copyable example

In the agent loop, wrap every bash call through the bundled wrapper:

& "<plugin install dir>\wrap-tool.ps1" `
    -Tool bash -Command "npm test" -Description "run tests"

State flow this triggers automatically:

  1. working — bash: run tests (pushed before the command runs)
  2. on exit 0: done — bash 完成
  3. on exit 1 (configurable): waiting — bash 等待审批 (exit=1)
  4. on other non-zero: error — bash 失败 (exit=N)

For other tools (read / write / edit), the agent pushes state directly
via notify-island.ps1 before and after each tool call. The Skill body in
skills/mcode-island/SKILL.md documents the exact timing.

Expected result

After each push the widget on the user's primary display updates within
~400 ms (one polling cycle). On click, the originating terminal tab regains
focus. The widget is intentionally hard to kill: Alt+F4 hides it (not
closes), and mcode-island show re-raises the hidden window in under one
second.

Requirements

requirement version / note
Windows 10 1809+ or 11 (uses WPF, user32 / kernel32)
PowerShell 5.1 (ships with Windows 10/11) or PowerShell 7
.NET WPF runtime 4.x (ships with Windows 10/11)
execution policy Bypass for this directory; not changed globally
network access none
accounts none
paid services none

The plugin contains no node_modules, no native binaries, no symlinks, no
installers, no private endpoints, no telemetry.

Network and data behavior

  • The widget never makes a network request.
  • No telemetry, no analytics, no auto-update checks.
  • All state lives under %APPDATA%\mcode-island\:
    status.json, caller.json, config.json, widget.pid, island.log,
    widget.log, show.signal.
  • The only registry write is to HKCU\Software\Microsoft\Windows\CurrentVersion\Run
    for logon auto-start (opt-in, user runs autostart.ps1 -Enable).
  • No data leaves the local machine.

Test evidence

This plugin was exercised on Windows 11 24H2 with PowerShell 5.1 against a
live MiniMax Code session. Concrete observations captured during development:

  • 59 state transitions in island.log over a multi-hour session
    (21 working / 14 done / 11 idle / 6 waiting / 5 thinking / 1 error).
  • All 6 states screenshot-verified (assets/state-*.png).
  • Full state-machine demo wrap-demo.png: thinkingworking
    waitingworkingdone on a real bash npm test run.
  • Click-to-focus round-trip verified: from a Feishu tab, click the pill,
    focus jumps to the originating Windows Terminal tab (HWND consistent).
  • wrap-tool.ps1 exit-code semantics: 0 → done, 1 → waiting (default,
    configurable via -WaitingExitCodes), other → error.

npm run check result

Validator output for the hosted plugin directory:

OK   plugin antianqi/mcode-island

The 8 unrelated FAIL lines in npm run check are pre-existing on
upstream/main (other contributors' hosted plugins missing YAML
frontmatter); this PR does not touch them. The single npm test failure
(hosted-plugins.test.mjs:39) is a Windows-only path-separator mismatch
in the upstream test (plugins\alice\hello-world vs /plugins\/alice\/hello-world/)
and is unrelated to this PR.

Package contents

plugins/antianqi/mcode-island/
├── plugin.json                    # plugin manifest (Agent Plugins 1.0 schema)
├── README.md                      # full user-facing docs
├── LICENSE                        # Apache-2.0
├── mcode-island.ps1               # WPF widget main loop
├── mcode-island.cmd               # CLI shim: start/stop/status/show/pin/...
├── start-island.ps1               # launcher (forces STA + hidden console)
├── stop-island.ps1                # stop the widget
├── status-island.ps1              # print widget PID + recent log
├── show-island.ps1                # re-raise hidden widget
├── pin-island.ps1                 # lock click-to-focus target
├── autostart.ps1                  # register / unregister Windows logon
├── notify-island.ps1              # state-push helper (agents call this)
├── wrap-tool.ps1                  # all-in-one bash wrapper
├── skills/mcode-island/SKILL.md   # Skill consumed by the agent
└── assets/                        # screenshots embedded above

Limitations

  • Windows 10/11 only.
  • One widget per user session.
  • wrap-tool.ps1 only wraps bash; for read / write / edit the agent
    calls notify-island.ps1 directly.
  • No hover-expand, no token usage, no per-tool output yet (planned for v0.2,
    Tauri rewrite).

License

Apache-2.0.


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

…ax 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.
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.

@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 (27 tests), but the core Windows detector is not ready:

  • mcode-status-detect.ps1:79-80 hard-codes C:\Users\Administrator\... and scans messages.jsonl, while the repository runtime uses resolved data directories and ledger.jsonl. Ordinary installations therefore report mcode 已退出 instead of detecting state.
  • mcode-status-detect.ps1:117-118,228-243 returns no message when the file mtime is unchanged, so the advertised 60-second idle fallback is unreachable during inactivity.
  • start-island.ps1:15-22, stop-island.ps1, and the detector start/stop scripts trust stale PID files and can refuse startup or Stop-Process -Force an unrelated process after PID reuse. Validate executable/command-line identity before acting.
  • wrap-tool.ps1:47-56 advertises a bash wrapper but executes -Command with Invoke-Expression as PowerShell code, creating an injection/shell-semantics boundary that should be removed or explicitly documented.
  • The quick-start commands are invalid PowerShell: README.md:82-90 uses %PLUGIN_DIR% and -Enable, but autostart.ps1:7-10 only supports -Action Enable.
  • start-island.ps1:40-44 waits for about to ShowDialog, a log message the widget never emits, so readiness is always reported as waiting.

Please fix the detector data-path/session contract, idle logic, PID validation, and launch/docs issues 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).
@antianqi

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed bad0868 with fixes for all six items. Quick recap:

Code fixes

Local verification

  • node scripts/validate.mjs reports OK plugin antianqi/mcode-island.
  • wrap-tool.ps1 4-state matrix exercised locally: -ExitCode 0 → done, 1 → waiting, 2 → error, omitted → working.
  • detector v0.2.1 settles into idle :: 已静默 195s after 65s with no new events.

Not in this push (out of scope of the review)

  • Detector still polls every 1s. Worker-thread leak (PS 5.1 + pipeline cmdlets) is observed; documenting separately, planning a FileSystemWatcher-based replacement in v0.3.
  • No change to the public Skill surface area (still one Skill, mcode-island).

Ready for another pass.

antianqi added a commit to antianqi/mcode-island that referenced this pull request Aug 22, 2026
This standalone mirror is now in lockstep with the in-flight PR #17
(MiniMax-AI/MiniMax-Code-Plugins#17), commit bad0868.

Changes since v0.1.0:

  + mcode-status-detect.ps1     v0.2 detector daemon
  + start-detect-island.ps1
  + stop-detect-island.ps1
  + status-detect-island.ps1
  M README.md                    detector + wrap-tool new API + -Action Enable
  M mcode-island.cmd             detect-on / detect-off / detect-status subcommands
  M skills/mcode-island/SKILL.md detector + new wrap-tool two-step pattern
  M start-island.ps1             PID + cmdline check; readiness via MainWindowHandle
  M stop-island.ps1              PID + cmdline check (refuse on PID reuse)
  M wrap-tool.ps1                removed Invoke-Expression; status-only; -ExitCode arg

Detector resolves the mcode install root at startup by regexing the mcode
node process command line (matched on @minimax-ai/code/cli.js), with
fallbacks to $env:USERPROFILE/.minimax-code, $env:APPDATA/minimax-code, and
the current working directory.

Validator: OK plugin antianqi/mcode-island (same as PR #17 head).
… 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 MiniMax-AI#17 review testing: 9 hours of "idle :: 已静默 49549s"
on a fresh detector after the v0.2.1 fixes were deployed.
@antianqi

Copy link
Copy Markdown
Contributor Author

One more fix on top of the v0.2.1 review fixes (commit 6e99c0b).

Bug: Get-LatestSessionFile always preferred ledger.jsonl when present, regardless of which file was more recently written. mcode v0.2.x leaves a ledger.jsonl from each session around, and once the session ends the ledger stops being written but the file remains. With even one stale ledger lying around:

  1. The detector reads the stale ledger every poll (newer messages.jsonl is ignored).
  2. The stale ledger's LastWriteTime is hours/days old.
  3. The 60s idle fallback fires: $inferred = @{ state='idle'; message="已静默 Ns" } with N = seconds since the ancient mtime.
  4. The widget stays frozen on "idle :: 已静默 49549s" indefinitely.

Verified in real use: the v0.2.1 detector was running for 9 hours showing exactly that — 49549s matched a 13.76h-old ledger whose last line was {"action":"test ledger 1"} test residue from earlier debugging.

Fix: compare LastWriteTime and pick whichever is newer. Fall back to ledger only if messages is absent (preserves the original fallback contract for systems where only ledger exists). Never let a stale ledger shadow a live messages.jsonl.

-  # 优先 ledger.jsonl 最新的;如果同 session 有 ledger 就用 ledger
-  $ledger = $all | Where-Object { $_.Name -eq $FNAME_LEDGER } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
-  if ($ledger) { return $ledger }
-  $msgs = $all | Where-Object { $_.Name -eq $FNAME_MESSAGES } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
-  return $msgs
+  # mcode v0.2.x writes messages.jsonl live; ledger.jsonl is best-effort and may
+  # be left behind by an old session. Always pick whichever file is most
+  # recently touched, otherwise a stale ledger would dominate and the
+  # 60s-idle fallback would fire against ancient timestamps.
+  $ledger = $all | Where-Object { $_.Name -eq $FNAME_LEDGER } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
+  $msgs   = $all | Where-Object { $_.Name -eq $FNAME_MESSAGES } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
+  if ($ledger -and $msgs) {
+    if ($ledger.LastWriteTime -ge $msgs.LastWriteTime) { return $ledger }
+    return $msgs
+  }
+  if ($ledger) { return $ledger }
+  if ($msgs) { return $msgs }
+  return $null

Verified live: after restart the detector now flips through working (matched a live bash toolCall with the actual command as message) and done correctly, instead of being stuck on the 13.76h phantom idle.

Pushed as 6e99c0b on add-mcode-island.

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.
@antianqi

Copy link
Copy Markdown
Contributor Author

One more on top of 6e99c0b — commit db73c11.

Bug: 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 externally-pushed. With no source on agent-pushed states, the detector treated every agent push as "no current status" and immediately overwrote it — most often with idle (60s fallback), even when the agent had just pushed working or thinking.

Concretely: notify-island.ps1 -State working would survive for ~1 second before the detector's next poll clobbered it back to idle. The manual notify tool was useless for any state the detector cared about, and the wrap-tool.ps1 -State working wrap pattern would never show on the pill.

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

  • agent push of working/thinking/done → preserved (detector-inferred working/thinking/done is not "settled", so the takeover branch is not taken when the current entry is not the detector's own);
  • agent push of idle/error → can be taken over by detector-inferred idle/error, 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 in the JSON.

 $payload = [PSCustomObject]@{
   state    = $State
   message  = $Message
   progress = $Progress
   ts       = $ts
+  source   = 'agent'
 } | ConvertTo-Json -Compress

So on top of the v0.2.1 review fixes there are now two late commits:

  • 6e99c0bGet-LatestSessionFile mtime-aware ledger vs messages pick
  • db73c11notify-island.ps1 tags its writes with source='agent'

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.
@antianqi

Copy link
Copy Markdown
Contributor Author

One more on top of db73c11 — commit 1b8eae4.

Bug: 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.

(The user-visible symptom masked as "9 hours of idle :: 已静默 49549s" is the other bug from 6e99c0b; this commit fixes the underlying cause of why a fresh detector, after that fix, would have eventually re-frozen.)

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 -RawFile.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 and the tens of thousands within hours). State transitions (workingdoneworking) still fire correctly. The 60s-idle fallback still fires correctly. No new code paths in the inference logic; all behavior is identical from the widget's perspective.

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.

-  $candidates = Get-ChildItem -Path $mcodeActiveDir -Filter '*.json' -ErrorAction SilentlyContinue
-  foreach ($f in $candidates) {
-    $j = Get-Content $f.FullName -Raw | ConvertFrom-Json
-    $targetPid = [int]$j.pid
-    $proc = Get-Process -Id $targetPid -ErrorAction SilentlyContinue
+  foreach ($f in [System.IO.Directory]::EnumerateFiles($mcodeActiveDir, '*.json')) {
+    $raw = [System.IO.File]::ReadAllText($f)
+    $j = $raw | ConvertFrom-Json
+    $targetPid = [int]$j.pid
+    $proc = [System.Diagnostics.Process]::GetProcessById($targetPid)

So on top of the v0.2.1 review fixes there are now three late commits:

  • 6e99c0bGet-LatestSessionFile mtime-aware ledger vs messages pick
  • db73c11notify-island.ps1 tags its writes with source='agent'
  • 1b8eae4 — kill the pipeline-thread leak that would have eventually re-frozen any detector this plugin ships

@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 a8ecc57 into MiniMax-AI:main Aug 25, 2026
1 check passed
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 26, 2026
Adds a Plugin-format Hooks declaration under `io.minimax.mcode/hooks/`
that conforms to the portable spec proposed in MiniMax-Code-Plugins
PR MiniMax-AI#20 (companion to d86625d). mcode 0.2.4 already ships the runtime
dispatch path for five of the twelve events; the remaining seven are
forward-looking and declared so the validator can warn on them.

The agent does not need to call `notify-island.ps1` manually when
the runtime wires the Hooks path. The detector-based fallback in
`mcode-status-detect.ps1` continues to run for everything else, so
this change is strictly additive: no existing capability is removed
or renamed.

## What changed

- `plugin.json`: bumped 0.2.1 → 0.3.0, declared
  `extensions.io.minimax.mcode.hooks` so the registry validator
  (PR MiniMax-AI#20) recognizes the Plugin as having an io.minimax.mcode
  client extension.
- `io.minimax.mcode/hooks/hooks.json`: 12-event declaration using
  only the portable field vocabulary (`command`, `args`, `env`,
  `cwd`, `matcher`, `pattern`, `regex`, `glob`, `timeout`,
  `timeoutMs`, `once`). No reserved fields. `PLUGIN_ROOT` is used
  for the script path; no host-absolute literals.
- `io.minimax.mcode/hooks/scripts/_lib.ps1`: shared helper exporting
  `Read-HookStdin`, `Push-Island`, `Test-IsSelfPush`,
  `Format-ToolSummary`. Loaded via dot-source from every event
  script. The self-push filter avoids recursive state churn when
  the agent calls `notify-island.ps1` directly through Bash.
- `io.minimax.mcode/hooks/scripts/<event>.ps1` x 12: one script
  per event. State mapping:

  | event             | pill state  | notes |
  | ----------------- | ----------- | ----- |
  | SessionStart      | idle        | |
  | SessionEnd        | idle        | |
  | UserPromptSubmit  | thinking    | |
  | PreToolUse        | working     | skips self-push |
  | PostToolUse       | done/error  | heuristic on tool_result |
  | Stop              | done        | |
  | PreCompact        | thinking    | |
  | Notification      | idle        | |
  | SubagentStart     | working     | CODEX only |
  | SubagentStop      | done        | CODEX only |
  | PermissionRequest | waiting     | returns `ask` (observer opt-in, see PR MiniMax-AI#20 §Decision semantics) |
  | PermissionDenied  | error       | |

- `permission-request.ps1`: returns `{"decision":"ask",...}`, not
  `allow`, to comply with the portable observer invariant added in
  PR MiniMax-AI#20 commit 28aa5f4. The 0.2.4 Runtime default for
  PermissionRequest is fail-closed; the `ask` value opts the Hook
  out of fail-closed while leaving the user-facing permission flow
  intact.
- `scripts/smoke.mjs`: pre-submit self-check. Zero dependencies
  (Node 18+ stdlib only), cross-platform. Validates `plugin.json`
  shape, the `extensions.io.minimax.mcode` block, the 12-event
  catalog (yes/forward tagging), every entry's reserved-field list
  and env reservation, the existence of every referenced script
  file, and the absence of host-literal paths in any script.
- `SKILL.md` / `README.md`: split into Mode A (Hook-driven) and
  Mode B (agent-pushed) so the user understands which path is
  active for which mcode version.
- `.gitattributes`: force LF for all source files. PowerShell 5.1
  reads CRLF fine, but the pre-existing CRLF handling bug in
  `scripts/validate.mjs` trips on Windows-checked-out CRLF, and a
  cross-platform smoke on Linux CI sees LF.

## Test evidence

End-to-end smoke (15/15) at @minimax-ai/code@0.2.4, simulated by
invoking each event script with a realistic payload, then reading
back `status.json` and verifying the multi-writer semantics with
the Runtime's own status detector:

    step=SessionStart           got=idle       src=agent      OK
    step=UserPromptSubmit       got=thinking   src=agent      OK
    step=PreToolUse-Bash        got=working    src=agent      OK
    step=PostToolUse-Bash       got=done       src=agent      OK
    step=PreToolUse-Read        got=working    src=agent      OK
    step=PostToolUse-Read       got=done       src=agent      OK
    step=PreCompact             got=thinking   src=agent      OK
    step=Stop                   got=done       src=agent      OK
    step=SubagentStart          got=working    src=agent      OK
    step=SubagentStop           got=done       src=agent      OK
    step=PermissionRequest      got=waiting    src=agent      OK
    step=PermissionDenied       got=error      src=agent      OK
    step=PreToolUse-self-push   got=error      src=agent      OK   (no change, filter applied)
    step=Notification           got=idle       src=agent      OK
    step=SessionEnd             got=idle       src=agent      OK
    ----
    summary: 15 pass, 0 fail

`scripts/smoke.mjs` on the in-repo tree:

    mcode-island v0.3.0 self-check
    [OK  ] plugin.json parses
    [OK  ] plugin.json: $schema is agent-plugins 1.0.0
    [OK  ] plugin.json: version is "0.3.0"
    [OK  ] plugin.json: extensions.io.minimax.mcode is present
    [OK  ] plugin.json: extensions.io.minimax.mcode.hooks resolves to io.minimax.mcode/hooks/hooks.json
    [OK  ] io.minimax.mcode/hooks/hooks.json parses
    [WARN] event "Stop"             is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "PreCompact"       is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "Notification"     is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "SubagentStart"    is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "SubagentStop"     is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "PermissionRequest" is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "PermissionDenied"  is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [OK  ] hooks.json[<event>]: script <name>.ps1 exists   x 12
    [OK  ] _lib.ps1: shared helper present
    [OK  ] <script>.ps1: no hardcoded host paths   x 13
    ----
    summary: 39 pass, 7 warn, 0 fail

The 7 WARN entries are the spec allowlist tagging (PR MiniMax-AI#20
"Empirical event catalog" table); they are expected and warn-only.

## Design compliance

- Agent Plugins 1.0 conformance preserved. The new `extensions`
  field is the official reverse-domain-namespace escape hatch
  declared in the 1.0 spec; no root-manifest field is overloaded.
- Cross-platform. Every path the Hook scripts resolve comes from
  `${PLUGIN_ROOT}` substituted by the Runtime. No host-absolute
  literals, no drive letters, no `/Users/` or `/home/` paths.
  `.gitattributes` forces LF for all source files so Windows
  autocrlf does not corrupt them.
- Self-disclosure. `SKILL.md`, `plugin.json` description, and
  `README.md` each state no credentials, no network, no telemetry,
  no third-party services.
- Atomic write. The `notify-island.ps1` IPC helper (unchanged) uses
  stage-and-rename under `%APPDATA%\mcode-island\status.json`; the
  previous state file is preserved on failure.
- Companion (not replacement) of the proposal. The Hook extension
  follows PR MiniMax-AI#20's portable spec verbatim. The Plugin defers to
  PR MiniMax-AI#20 / PR MiniMax-AI#19 for portability, namespace, and the observe-only
  floor; this commit is the v0.3.0 instantiation.

## Out of scope (intentionally)

- Does not modify `docs/plugin-compatibility.md` to claim Hook
  support. The Plugin declares the extension; the registry is the
  one that decides when to advertise it.
- Does not modify `docs/security-model.md`.
- Does not propose a different namespace or event catalog.
- Does not add runtime code to mcode 0.2.4; the Plugin runs against
  the existing Runtime.
- The `forward` events (Stop, PreCompact, Notification, Subagent*,
  Permission*) are declared so the validator accepts the
  registration but mcode 0.2.4 may or may not dispatch them. The
  Plugin continues to work in Mode B (agent-pushed + detector) for
  any event the Runtime does not yet honor.

## Refs

- MiniMax-Code-Plugins PR MiniMax-AI#20 (companion proposal,
  proposals/hooks-detailed-spec.md) — portable spec, validator,
  example fixture.
- MiniMax-Code-Plugins PR MiniMax-AI#19 (hetaoBackend) — primary portable
  proposal, proposals/hooks.md.
- @minimax-ai/code@0.2.4 (npm, 2026-08-24) — Runtime release notes.
- Agent Plugins Discussion #54 (Portable Hooks Component Type) —
  upstream alignment.
- MiniMax-Code-Plugins PR MiniMax-AI#17 (previous mcode-island v0.2.1) —
  baseline that this commit supersedes.
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