Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- run: npm test
- run: npm run pack

# The Windows MiniMax suite gates a release, not every PR or push.
# The Windows host suites gate releases.
windows-minimax:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: windows-latest
Expand All @@ -48,6 +48,8 @@ jobs:
cache: npm
- run: npm ci
- run: npm run test:minimax
- run: npm run test:zcode
- run: npm run test:stepcode
- run: npm run pack

# A release is a tag vX.Y.Z a maintainer pushes on main; nothing on main names a version. The tag's version is
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ The plugin is the access layer: shared logic lives in `lib/`, and each host is a
| Grok CLI 1.0.70+ | Supported | `hosts/grok`, command hooks; the strategy reaches the model at the turn's first tool result; installs `dist/grok` |
| Qoder CLI (`qodercli`) 1.1.79+ | Supported | `hosts/qoder`, command hooks; installs `dist/qoder` |
| MiniMax Code CLI 0.7.4 / Desktop 3.1.6 | Supported locally | `hosts/minimax`, native hooks; installs `dist/minimax` |
| ZCode CLI | Supported locally on Windows | `hosts/zcode`, native hooks; installs `dist/zcode` |
| Step Code CLI 0.1.7 | Draft, Windows | `hosts/stepcode`, a native resident extension; installs `dist/stepcode` |
| Muse Code | Not yet | No reviewed adapter yet |
| OpenAI Dots, Claude Duet, Grok Bot, Meta Muse | Not yet | Cloud agents without hooks; need Evolver's remote MCP and routines |

Expand Down Expand Up @@ -150,6 +152,13 @@ injections, undelivered reports), so nothing is lost on the way over.
`evolver-jobs`, `evolver-jobs job <id>` or `evolver-jobs cancel <id>` to inspect or cancel them.
[The MiniMax adapter guide](hosts/minimax/dist/README.md) covers updates, session-model distillation, private
configuration copies and recovery when a host exits without `SessionEnd`.
- **ZCode CLI on Windows:** build `dist/zcode` with `npm run pack` and add that folder to the CLI's `plugins.dirs`.
Enable plugins and hooks, then start a new session. The [ZCode adapter guide](hosts/zcode/dist/README.md)
covers native verification and private distillation with a standalone CLI bundle.
- **Step Code CLI 0.1.7 on Windows (draft):** build `dist/stepcode` with `npm run pack`, then run
`step install ./dist/stepcode` and start a new session. This uses Step's native extension interface.
Read the [Step Code adapter guide](hosts/stepcode/dist/README.md) for foreground verification requirements
and the native double Ctrl-C cleanup limitation before trying this draft.
- **Antigravity:** `agy plugin uninstall evolver-antigravity-plugin`, then from the `release` branch,
`git clone --depth 1 --branch release https://github.com/EvoMap/evolver-agent-plugin.git` and
`agy plugin install evolver-agent-plugin/dist/antigravity`.
Expand Down
14 changes: 14 additions & 0 deletions hosts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ From each host's reviewed standalone plugin; bold is where it differs from the d
| State | default | **plugin data folder** | default | default | default | default | default | default | default | default | PLUGIN_DATA; persistent recall/auto-distill preferences |
| Installed from | **`dist/claude-code` on the `release` branch** (`git-subdir`) | **`dist/workbuddy` on the `release` branch** (`git-subdir`); **the old data folder is moved in** | the repo root (`.cursor-plugin`) | **`dist/antigravity` on the `release` branch** | **`dist/opencode` on the `release` branch (an npm package, not published yet)** | **its MCP config** | **`dist/codex` on the `release` branch** (`git-subdir`, `.agents/plugins/marketplace.json`) | **`dist/dsh` on the `release` branch, as `@evomap/dsh-evolver` (`dsh plugin add -w <folder>`; npm not published yet)** | **`dist/grok` on the `release` branch** (`git-subdir`, `.grok-plugin/marketplace.json`) | **`dist/qoder` on the `release` branch** (`git-subdir`, `.qoder-plugin/marketplace.json`) | dist/minimax locally; scripts/install-minimax.mjs stages install/update |

ZCode CLI on Windows uses native session and turn IDs and foreground Bash exit codes. The prompt hook returns
`additionalContext`. The adapter waits for the native `turn_usage` record before using completed work and closes
cancelled turns without a reuse report or distillation evidence. State lives in `ZCODE_PLUGIN_DATA` when the host
sets it. Headless distillation uses the configured standalone CLI bundle with a private copy of the recorded provider route.
[The ZCode adapter guide](zcode/dist/README.md) describes local installation and the runtime configuration paths.

Step Code CLI 0.1.7 on Windows uses a resident native extension. Recipe guidance is projected into each model request
of the current prompt and is not persisted into the transcript. A plugin turn spans native model steps and ends only
at `agent_settled`; a latched abort, unfinished command or unsupported inline/background check prevents reporting
and candidate creation. Native shell starts and terminal exit metadata are paired by call. SDK completion receives
only a private distillation prompt, no tools or native session transcript, and retains the captured account/model route.
Preferences and private data live under the selected `STEP_CODING_AGENT_DIR` in `evolver-plugin`.
[The Step Code draft guide](stepcode/dist/README.md) records installation, supported commands and acceptance limits.

## Adding a host

MiniMax Code's local adapter uses the hook defaults for `session_id`, `turn_id`, prompt and additional context.
Expand Down
29 changes: 29 additions & 0 deletions hosts/stepcode/dist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Evolver for Step Code CLI

This adapter targets Step Code CLI 0.1.7 on Windows. It is a native extension, loaded through Step's `pi.extensions` package manifest.

```powershell
npm ci
node scripts/pack.mjs stepcode dist/stepcode
step install ./dist/stepcode
```

Start a new session after installation or update. For a disposable profile, set `STEP_CODING_AGENT_DIR` to an absolute directory before starting Step. Evolver keeps its preferences, reports, session ranges and private drafts in that directory's `evolver-plugin` folder.

Recipe guidance is projected into the current prompt's model requests, without being appended to the native transcript. Native `run_command` or `bash` results supply verification evidence. Publication remains an explicit `/evolver-drafts publish <number>` action; ordinary verified work only creates private draft candidates.

Use a separate foreground `run_command` or `bash` call for verification. Inline `then_run` checks and background executions are not supported by this draft adapter; a turn using them is excluded from reuse reporting and candidates. A cancelled turn, including cancellation after a passing check, produces neither a reuse report nor a candidate.

Background completion uses the captured native provider, model, thinking level and account. If that route cannot be verified, the candidate waits rather than switching to the current account. It does not run another agent, load tools, or publish assets.

The `/evolver-*` commands expose status, configuration, draft review and explicit publication in the interactive CLI. This draft does not add a Step Code Desktop adapter.

`/evolver-config recall off` and `/evolver-config auto-distill off` save preferences for this Step profile. Environment overrides still take precedence. To remove the extension, run `step remove ./dist/stepcode` with the same installed source path; this does not delete Evolver's profile data.

## Draft acceptance limits

The native Step Code 0.1.7 Windows package has been exercised with a loopback model and Proxy for Recipe context, passing and failing checks, private drafts, and cancellation. Paid-provider and real Hub acceptance are still pending.

Esc and RPC abort stopped the fixture process in native tests. Double Ctrl-C can make Step exit without a terminal tool event while leaving its tool's shell/process alive. The adapter suppresses reports and candidates for that incomplete turn; it does not terminate processes owned by Step. This native runtime cleanup gate remains open.

SDK completion receives a cancellation signal and rejects late answers. Native credential resolution has no abort-signal interface; interruption of OAuth refresh and transport cleanup against a paid provider have not been verified.
119 changes: 119 additions & 0 deletions hosts/stepcode/dist/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 EvoMap

import { createHash } from 'node:crypto';

const variants = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
const reasons = new Set(['route_unresolved', 'route_changed', 'credential_missing', 'auth_unsupported', 'unsafe_runtime']);
const id = (value) => typeof value === 'string' && /^[\w.@:/+-]{1,256}$/.test(value);
const unavailable = (reason) => Object.assign(new Error('The captured Step Code model is unavailable'), { code: 'MODEL_UNAVAILABLE', reason });
const safeUnavailable = (error) => unavailable(error?.code === 'MODEL_UNAVAILABLE' && reasons.has(error.reason) ? error.reason : 'credential_missing');
const ordered = (value) => Array.isArray(value) ? value.map(ordered) : value && typeof value === 'object'
? Object.fromEntries(Object.keys(value).sort().map((key) => [key, ordered(value[key])])) : value;
const digestOf = (value) => createHash('sha256').update(JSON.stringify(ordered(value))).digest('hex');
const stopped = (signal) => new DOMException('Step Code completion stopped', signal.reason?.name === 'TimeoutError' ? 'TimeoutError' : 'AbortError');

async function withSignal(work, signal) {
if (signal.aborted) throw stopped(signal);
let abort;
try {
return await Promise.race([work(), new Promise((_resolve, reject) => {
abort = () => reject(stopped(signal));
signal.addEventListener('abort', abort, { once: true });
if (signal.aborted) abort();
})]);
} finally { if (abort) signal.removeEventListener('abort', abort); }
}

async function snapshotOf(ctx) {
const model = structuredClone(ctx?.model);
const original = digestOf(model);
const variant = ctx?.thinkingLevel ?? 'off';
const registry = ctx?.modelRegistry;
if (!id(model?.provider) || !id(model?.id) || !variants.has(variant)
|| typeof registry?.getProvider !== 'function' || typeof registry?.getApiKeyAndHeaders !== 'function') {
throw unavailable('route_unresolved');
}
const provider = registry.getProvider(model.provider);
if (provider?.id !== model.provider || typeof provider.streamSimple !== 'function') throw unavailable('unsafe_runtime');
const stream = provider.streamSimple;
const resolved = await registry.getApiKeyAndHeaders(model);
if (resolved?.ok !== true || typeof resolved.apiKey !== 'string' || !resolved.apiKey.trim()) throw unavailable('credential_missing');
const auth = structuredClone({ apiKey: resolved.apiKey, headers: resolved.headers, env: resolved.env });
if (digestOf(ctx.model) !== original || (ctx.thinkingLevel ?? 'off') !== variant
|| registry.getProvider(model.provider) !== provider || provider.streamSimple !== stream) throw unavailable('route_changed');
if (resolved.baseUrl) model.baseUrl = resolved.baseUrl;
return {
ctx, provider, stream, model, auth,
descriptor: { configDigest: digestOf({ model, variant, auth, defaults: { baseUrl: provider.baseUrl, headers: provider.headers } }),
selection: { providerId: model.provider, modelId: model.id, variant } },
};
}

/** Credentials stay in the active native process; only an opaque route digest enters session state. */
export function stepModel() {
let captured;
let generation = 0;
const requests = new Set();
const invalidate = () => {
captured = undefined;
for (const request of requests) request.abort();
};
return {
capture: true,
async captureContext(ctx) {
const attempt = ++generation;
try {
const next = await snapshotOf(ctx);
if (attempt !== generation) throw unavailable('route_changed');
if (next.descriptor.configDigest === captured?.descriptor.configDigest && next.provider === captured.provider && next.stream === captured.stream) {
captured.ctx = ctx;
} else { invalidate(); captured = next; }
return structuredClone(captured.descriptor);
} catch (error) {
if (attempt === generation) invalidate();
throw safeUnavailable(error);
}
},
configStamp: () => captured?.descriptor.configDigest ?? null,
async complete(descriptor, prompt, stopping) {
const binding = captured;
if (!binding || digestOf(descriptor) !== digestOf(binding.descriptor)) throw unavailable('route_unresolved');
const request = new AbortController();
const timer = setTimeout(() => request.abort(new DOMException('Deadline', 'TimeoutError')), 90_000);
timer.unref();
requests.add(request);
const signal = stopping ? AbortSignal.any([stopping, request.signal]) : request.signal;
const check = async () => {
const current = await withSignal(() => snapshotOf(binding.ctx).catch((error) => { throw safeUnavailable(error); }), signal);
if (signal.aborted) throw stopped(signal);
if (captured !== binding || current.descriptor.configDigest !== binding.descriptor.configDigest
|| current.provider !== binding.provider || current.stream !== binding.stream) throw unavailable('route_changed');
};
try {
await check();
const result = await withSignal(() => binding.stream.call(binding.provider, structuredClone(binding.model), {
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }], timestamp: Date.now() }], tools: [],
}, {
...structuredClone(binding.auth), maxTokens: 1_024, reasoning: binding.descriptor.selection.variant === 'off' ? undefined : binding.descriptor.selection.variant,
toolChoice: 'none', cacheRetention: 'none', signal, timeoutMs: 90_000, maxRetries: 0,
}).result(), signal).catch(() => {
if (signal.aborted) throw stopped(signal);
throw new Error('Step Code completion failed');
});
await check();
if (result?.role !== 'assistant' || result.provider !== binding.model.provider || result.model !== binding.model.id
|| result.api !== binding.model.api || result.stopReason !== 'stop' || !Array.isArray(result.content)
|| result.content.some((part) => !part || (part.type !== 'thinking' && (part.type !== 'text' || typeof part.text !== 'string')))) {
throw new Error('Step Code returned no complete answer');
}
const answer = result.content.filter((part) => part.type === 'text').map((part) => part.text).join('\n');
if (!answer.trim() || answer.length > 1_048_576) throw new Error('Step Code returned no complete answer');
const { input, output } = result.usage ?? {};
return { answer, ...(Number.isSafeInteger(input) && input >= 0 && Number.isSafeInteger(output) && output >= 0
&& { usage: { inputTokens: input, outputTokens: output } }) };
} finally { clearTimeout(timer); requests.delete(request); }
},
clear() { generation++; invalidate(); },
};
}
9 changes: 9 additions & 0 deletions hosts/stepcode/dist/pack.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entries": ["hosts/stepcode/dist/plugin.mjs"],
"root": {
"plugin.mjs": "hosts/stepcode/dist/root/plugin.mjs",
"package.json": "hosts/stepcode/dist/package.json",
"README.md": "hosts/stepcode/dist/README.md",
"LICENSE": "LICENSE"
}
}
14 changes: 14 additions & 0 deletions hosts/stepcode/dist/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@evomap/stepcode-evolver",
"version": "1.0.0",
"description": "Recipe recall, native verification evidence and private drafts for Step Code CLI.",
"type": "module",
"main": "./plugin.mjs",
"exports": { ".": "./plugin.mjs", "./package.json": "./package.json" },
"files": ["plugin.mjs", ".evolver-packed", "hosts/", "lib/", "README.md", "LICENSE"],
"keywords": ["pi-package", "stepcode", "evomap", "evolver"],
"repository": { "type": "git", "url": "git+https://github.com/EvoMap/evolver-agent-plugin.git", "directory": "hosts/stepcode/dist" },
"license": "MIT",
"engines": { "node": ">=22.13.0" },
"pi": { "extensions": ["./plugin.mjs"] }
}
Loading
Loading