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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/studio-no-more-subscription/easyloop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.data/
*.swp
.DS_Store
17 changes: 17 additions & 0 deletions plugins/studio-no-more-subscription/easyloop/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

Copyright 2026 bro

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
101 changes: 101 additions & 0 deletions plugins/studio-no-more-subscription/easyloop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# easyloop

Self-orchestrating loop Skill for MiniMax Code. When the user invokes
**easyloop**, the model reads a fixed menu of loop patterns + quality
levels + optional plugins, picks what fits the task, and calls the
`finish_plugin_selection` MCP tool. The tool persists the selection
**along with each loop's one-line description** under a
`context_compacted_id`, then returns a short plan. From that point on,
the model carries only the id forward; the original menu can be
re-injected with `recall_selection("<id>")`.

Ships with three Skills: `easyloop`, `easyloop-prototype`,
`easyloop-release`.

## Install

```bash
cp -r easyloop ~/.mcode/plugins/local/
mcode plugin enable easyloop@local
```

## Loop catalog

13 loop patterns (12 standalone + 1 chain). Each ships with a one-line
description persisted alongside the selection:

| Loop | One-line description |
| --- | --- |
| `explore` | Read and understand unfamiliar code; return a compressed summary. |
| `implement` | Write new code from a spec or plan. |
| `test` | Run tests, interpret failures, fix them until green. |
| `debug` | Diagnose a known bug, isolate root cause, patch it. |
| `refactor` | Improve structure or clarity without changing behavior. |
| `review` | Read code or a diff and surface concrete defects. |
| `document` | Write or update docs, docstrings, READMEs, comments. |
| `migrate` | Port code between frameworks, languages, or major versions. |
| `benchmark` | Profile and optimize performance, memory, or bundle size. |
| `security-audit` | Check for known vulnerability classes and fix them. |
| `deps-update` | Upgrade dependencies safely with tests + lockfile. |
| `release` | Version bump, changelog, tag, publish artifacts. |
| `chain` | Combine multiple loops in sequence (recommended for non-trivial work). |

## Quality levels

| Quality | Iteration cap | Verify (tests/lint/type) | When to use |
| --- | --- | --- | --- |
| `prototype` | 5 | off | Spike / exploration; throwaway code |
| `release` | 25 | on | Production / ship-ready output |

## MCP tools

| Tool | Purpose |
| --- | --- |
| `finish_plugin_selection` | Persist selection, return `context_compacted_id` + plan. |
| `recall_selection` | Re-inject the original menu + saved plan for an id. |
| `log_iteration` | Append an iteration audit entry. |
| `list_selections` | List all selections saved in this session. |

## Sub-agent strategy

The Skill body instructs the model to:

- Use the `Task` tool (`task` / `delegate` / `spawn_agent`) with
`run_in_background: true` for independent sub-tasks.
- Spawn 3–5 background scouts at a time for multi-file exploration.
- Aggregate results before the next iteration.
- For implementation, one background worker implements while the main
thread prepares tests / lint.

## Copyable example prompt

> "easyloop: refactor `src/auth/` to use async/await end-to-end and ship."

The model will:
1. Read the menu.
2. Call `finish_plugin_selection({ loops: ["explore","refactor","test","review"], quality: "release", plugins: ["mcode-think-filter"], notes: "auth async/await" })`.
3. Carry only the id forward.
4. Spawn background scouts in parallel.
5. Iterate refactor + test cycles up to the 25-turn cap.
6. `recall_selection("<id>")` if it needs to look at the menu again.

## Tests

```bash
cd easyloop
node --test lib/*.test.mjs
```

Covers:
- Catalog has 12+ loops with descriptions.
- `renderMenu` covers every loop and quality.
- `validateSelection` rejects empty/unknown loops; accepts valid ones.
- `saveSelection` / `readSelection` round-trip with descriptions preserved.
- `readSelection` rejects malformed ids (path traversal guard).

## Disclosure

- Persists `selections/<id>.json` and `iterations.jsonl` under
`${PLUGIN_DATA}`.
- Network: zero.
- Native binaries: none.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json",
"hooks": {
"SessionStart": [
{
"command": "node",
"args": [
"${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/log-loop.mjs",
"--data",
"${PLUGIN_DATA}",
"--phase",
"session-start"
],
"timeout": 5000,
"once": false
}
],
"Stop": [
{
"command": "node",
"args": [
"${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/log-loop.mjs",
"--data",
"${PLUGIN_DATA}",
"--phase",
"stop"
],
"timeout": 5000,
"once": false
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env node
// SessionStart / Stop observer for the easyloop plugin. Ensures
// ${PLUGIN_DATA}/selections/ exists and appends a small audit entry.

import { argv, env } from 'node:process';
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

function parseArgs(args) {
const out = { data: null, phase: 'session-start' };
for (let i = 0; i < args.length; i += 1) {
const a = args[i];
if (a === '--data') { out.data = args[i + 1]; i += 1; }
else if (a === '--phase') { out.phase = args[i + 1]; i += 1; }
}
return out;
}

function expandData(value) {
if (typeof value !== 'string') return null;
if (value.startsWith('${PLUGIN_DATA}')) {
const r = env.PLUGIN_DATA;
return r ? r + value.slice('${PLUGIN_DATA}'.length) : null;
}
return value;
}

async function main() {
const args = parseArgs(argv.slice(2));
const dataDir = expandData(args.data);
if (!dataDir) return;
await mkdir(join(dataDir, 'selections'), { recursive: true });
await writeFile(
join(dataDir, 'session.log'),
JSON.stringify({ ts: new Date().toISOString(), phase: args.phase, pid: process.pid }) + '\n',
{ flag: 'a' },
);
}

main().catch(() => {});
87 changes: 87 additions & 0 deletions plugins/studio-no-more-subscription/easyloop/lib/catalog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// catalog.mjs
//
// Static catalog of loops and quality levels. The plugin list passed to
// renderMenu() comes from lib/discover.mjs (CLI + filesystem scan).
// Pure data; no I/O.

import { BUILTIN_OPTIONAL } from './discover.mjs';

export const LOOPS = [
{ name: 'explore', description: 'Read and understand unfamiliar code; return a compressed summary.' },
{ name: 'implement', description: 'Write new code from a spec or plan.' },
{ name: 'test', description: 'Run tests, interpret failures, fix them until green.' },
{ name: 'debug', description: 'Diagnose a known bug, isolate root cause, patch it.' },
{ name: 'refactor', description: 'Improve structure or clarity without changing behavior.' },
{ name: 'review', description: 'Read code or a diff and surface concrete defects.' },
{ name: 'document', description: 'Write or update docs, docstrings, READMEs, comments.' },
{ name: 'migrate', description: 'Port code between frameworks, languages, or major versions.' },
{ name: 'benchmark', description: 'Profile and optimize performance, memory, or bundle size.' },
{ name: 'security-audit',description: 'Check for known vulnerability classes and fix them.' },
{ name: 'deps-update', description: 'Upgrade dependencies safely with tests + lockfile.' },
{ name: 'release', description: 'Version bump, changelog, tag, publish artifacts.' },
{ name: 'chain', description: 'Combine multiple loops in sequence (recommended for non-trivial work).' },
];

export const QUALITIES = [
{ name: 'prototype', description: 'Fast, throwaway, minimal tests, no docs.', iterationCap: 5, verify: false },
{ name: 'release', description: 'Full tests, type safety, docs, error handling.', iterationCap: 25, verify: true },
];

// Backwards-compatible default (built-in optionals). Tests that don't
// care about discovery still work without changing their imports.
export const OPTIONAL_PLUGINS = BUILTIN_OPTIONAL.map((p) => ({
name: p.name,
description: p.description,
}));

// Render the selection menu as plain text the model reads in the Skill
// body. Pass `plugins` from lib/discover.mjs to include every installed
// plugin's one-line description; pass `null`/`undefined` to fall back
// to the built-in optionals; pass `[]` to explicitly show "none
// discovered".
export function renderMenu(plugins) {
// undefined/null → built-in fallback; [] → empty (caller knows there
// are no plugins).
const list = plugins === undefined || plugins === null
? BUILTIN_OPTIONAL
: plugins;
const builtinNames = new Set(BUILTIN_OPTIONAL.map((p) => p.name));
const lines = [];
lines.push('LOOP PATTERNS:');
for (const l of LOOPS) {
lines.push(` ${l.name.padEnd(16)} — ${l.description}`);
}
lines.push('');
lines.push('QUALITY LEVEL:');
for (const q of QUALITIES) {
lines.push(` ${q.name.padEnd(16)} — ${q.description} (cap ${q.iterationCap} turns, verify=${q.verify})`);
}
lines.push('');
lines.push('OPTIONAL PLUGINS (you may enable any of these for this task):');
if (list.length === 0) {
lines.push(' (none discovered)');
} else {
for (const p of list) {
const tag = builtinNames.has(p.name) ? ' (recommended)' : '';
const desc = (p.description && p.description.length > 0)
? p.description
: '(no description provided by manifest)';
lines.push(` ${p.name.padEnd(28)} — ${desc}${tag}`);
}
}
lines.push('');
lines.push('PARALLELISM:');
lines.push(' Use the Task tool (`task`/`delegate`/`spawn_agent`) with `run_in_background: true` whenever');
lines.push(' sub-tasks are independent. Maximize parallel calls in one turn. Spawn 3–5 background scouts');
lines.push(' at a time for multi-file exploration, then aggregate.');
return lines.join('\n');
}

// Look up loop metadata by name.
export function loopByName(name) {
return LOOPS.find((l) => l.name === name) || null;
}

export function qualityByName(name) {
return QUALITIES.find((q) => q.name === name) || null;
}
Loading