refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes, cline, codebuddy and pi end to end: capability descriptors, installer branches and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters, hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi migrations, Kimi payload normalization in the hook guards, dead hostBehaviors vocabulary, launcher home probes, fixtures, runtime-specific tests and the prose that presented them as supported. Installer output for the six kept runtimes is byte-identical to before the prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and read-injection-scanner are left in place pending a decision.
This commit is contained in:
@@ -1,256 +0,0 @@
|
||||
/**
|
||||
* Cline SDK binding — AgentPlugin + createAgentModel adapters
|
||||
* (ADR-1239 Phase D / #2090).
|
||||
*
|
||||
* Two Context7-verified UPGRADES the file-convention projection ignored, now
|
||||
* delivered through the negotiated `hookBus: host` + `modelMode: active`
|
||||
* interface points:
|
||||
*
|
||||
* UPGRADE 1 — `AgentPlugin.hooks.beforeTool` planning-artifact guard.
|
||||
* Re-implements the `.clinerules/hooks/PreToolUse` file-convention hook
|
||||
* (issue #787) as a real Cline SDK AgentPlugin. Guard semantics are
|
||||
* preserved EXACTLY: fail-open, cancel (skip) write-class calls targeting
|
||||
* `.planning/`, pass through everything else. The SDK maps the file hook's
|
||||
* `{cancel:true, errorMessage}` to `{skip:true, reason}` (beforeTool
|
||||
* contract).
|
||||
* Cite: https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx
|
||||
* https://github.com/cline/cline/blob/main/sdk/packages/agents/README.md
|
||||
*
|
||||
* UPGRADE 2 — `DefaultGateway.createAgentModel({providerId, modelId})`.
|
||||
* Resolves MSD's per-subagent `model_overrides` / `model_profile_overrides`
|
||||
* (already used for OpenCode/Codex passive hosts) into the createAgentModel
|
||||
* call params for cline's active model mode. The host gateway owns the
|
||||
* actual model instantiation; this binding resolves WHICH model an
|
||||
* overridden subagent should use.
|
||||
* Cite: https://github.com/cline/cline/blob/main/docs/sdk/reference/gateway.mdx
|
||||
* https://github.com/cline/cline/blob/main/sdk/packages/llms/README.md
|
||||
*
|
||||
* This module is PURE (no I/O, no SDK import): the real `@cline/sdk` is a
|
||||
* fast-moving package set not linked at build/test time, so the binding exposes
|
||||
* the decision functions a host plugin/gateway would call. Tests drive payloads
|
||||
* through them directly (same mock-the-SDK pattern as the VS Code reference
|
||||
* binding, tests/fixtures/vscode-host-binding.cjs).
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 1 — beforeTool planning-artifact guard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Write-class tool-verb detector. Matches the SAME regex as the #787
|
||||
* PreToolUse file-convention hook so the guard behaves identically.
|
||||
* Case-insensitive (the SDK delivers tool names in varying case).
|
||||
*/
|
||||
export const WRITE_TOOL_PATTERN = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
|
||||
|
||||
/**
|
||||
* `.planning/` path detector. Matches `.planning` preceded by start-of-string
|
||||
* or a path separator (posix `/` or windows `\`) and followed by a separator or
|
||||
* end-of-string — so `.planning-readme.txt` is NOT falsely matched. Mirrors the
|
||||
* PreToolUse hook's `(^|[\\/])\.planning([\\/]|$)` exactly.
|
||||
*/
|
||||
export const PLANNING_PATH_PATTERN = /(^|[\\/])\.planning([\\/]|$)/;
|
||||
|
||||
/**
|
||||
* The user-visible reason returned when a `.planning/` write is blocked.
|
||||
* Preserves the PreToolUse hook's errorMessage text so the guard behaves
|
||||
* identically to the user (cancel→skip, errorMessage→reason).
|
||||
*/
|
||||
export const PLANNING_GUARD_REASON: string = Object.freeze(
|
||||
'MSD: .planning/ artifacts are managed by MSD workflows. Edit them only through a /msd-* command, not directly.',
|
||||
);
|
||||
|
||||
/**
|
||||
* Path-bearing field-name detector. Only PATH-keyed field values are inspected,
|
||||
* so a document that merely mentions ".planning/" in its body content is never
|
||||
* falsely blocked. Mirrors the PreToolUse hook's PATH_KEY regex exactly.
|
||||
*/
|
||||
const PATH_KEY_PATTERN = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
|
||||
|
||||
type BeforeToolPayload = {
|
||||
tool?: { name?: unknown } | string | null | undefined;
|
||||
input?: unknown;
|
||||
};
|
||||
|
||||
type BeforeToolDecision = { decision: 'skip'; reason: string } | { decision: 'allow'; reason?: undefined };
|
||||
|
||||
/**
|
||||
* Collect PATH-bearing string field values from an object tree, mirroring the
|
||||
* PreToolUse hook's bounded walk. Pure; never throws.
|
||||
*/
|
||||
function collectPathValues(root: unknown): string[] {
|
||||
const paths: string[] = [];
|
||||
const walk = (v: unknown, depth: number): void => {
|
||||
if (depth > 5 || paths.length > 64) return;
|
||||
if (Array.isArray(v)) {
|
||||
for (const x of v) walk(x, depth + 1);
|
||||
return;
|
||||
}
|
||||
if (v && typeof v === 'object') {
|
||||
const obj = v as Record<string, unknown>;
|
||||
for (const k of Object.keys(obj)) {
|
||||
const val = obj[k];
|
||||
if (typeof val === 'string' && PATH_KEY_PATTERN.test(k)) {
|
||||
paths.push(val);
|
||||
} else {
|
||||
walk(val, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(root, 0);
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tool name from a beforeTool payload's `tool` field, which may be a
|
||||
* string or an object with a `name` property. Returns '' when absent (treated
|
||||
* as non-write-class → allow, fail-open).
|
||||
*/
|
||||
function resolveToolName(tool: BeforeToolPayload['tool']): string {
|
||||
if (!tool) return '';
|
||||
if (typeof tool === 'string') return tool;
|
||||
const name = tool.name;
|
||||
return typeof name === 'string' ? name : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* The pure guard decision: given a beforeTool payload, decide skip (cancel) or
|
||||
* allow. Fail-OPEN — any malformed input, missing tool, or thrown error returns
|
||||
* 'allow' (the guard never blocks on a defect, mirroring the PreToolUse hook).
|
||||
*
|
||||
* @returns `{decision:'skip', reason}` for a write-class call targeting
|
||||
* `.planning/`; `{decision:'allow'}` for everything else.
|
||||
*/
|
||||
export function evaluateBeforeTool(payload: BeforeToolPayload | null | undefined): BeforeToolDecision {
|
||||
try {
|
||||
if (!payload) return { decision: 'allow' };
|
||||
const toolName = resolveToolName(payload.tool);
|
||||
if (!toolName) return { decision: 'allow' };
|
||||
const isWrite = WRITE_TOOL_PATTERN.test(toolName);
|
||||
if (!isWrite) return { decision: 'allow' };
|
||||
const paths = collectPathValues(payload.input);
|
||||
const isPlanningPath = (s: string): boolean => PLANNING_PATH_PATTERN.test(s);
|
||||
if (paths.some(isPlanningPath)) {
|
||||
return { decision: 'skip', reason: PLANNING_GUARD_REASON };
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
} catch {
|
||||
// Fail-open: a defect in the guard never blocks the user's operation.
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cline `AgentPlugin` shape (Context7 /cline/cline). A plugin implements
|
||||
* `setup({agentId})` returning `{hooks, tools}`. The `beforeTool` hook returns
|
||||
* `{skip:true, reason}` to block or `undefined` to allow.
|
||||
*
|
||||
* This object is the portable plugin a host loads from `~/.cline/plugins/`
|
||||
* (analogous to `.opencode/plugins/msd-core.js`). Its `beforeTool` delegates to
|
||||
* the pure `evaluateBeforeTool` so the decision logic is testable without the
|
||||
* SDK linked.
|
||||
*/
|
||||
export const clineMsdPlugin: {
|
||||
name: string;
|
||||
setup: (ctx: { agentId?: string }) => {
|
||||
hooks: {
|
||||
beforeTool: (payload: BeforeToolPayload) => { skip: true; reason: string } | undefined;
|
||||
};
|
||||
};
|
||||
} = Object.freeze({
|
||||
name: 'msd-planning-guard',
|
||||
setup(_ctx: { agentId?: string }) {
|
||||
return {
|
||||
hooks: {
|
||||
beforeTool(payload: BeforeToolPayload): { skip: true; reason: string } | undefined {
|
||||
const decision = evaluateBeforeTool(payload);
|
||||
return decision.decision === 'skip' ? { skip: true, reason: decision.reason } : undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 2 — createAgentModel model-override resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The fallback provider id when a model id does not match a known provider
|
||||
* family. Anthropic is cline's most common default; the host gateway retains
|
||||
* the final say over provider resolution.
|
||||
*/
|
||||
export const DEFAULT_CLINE_PROVIDER_ID: string = 'anthropic';
|
||||
|
||||
/**
|
||||
* Infer a `providerId` (the createAgentModel first arg) from a model id by
|
||||
* matching known provider families. Returns DEFAULT_CLINE_PROVIDER_ID for an
|
||||
* unrecognized or empty id (fail-safe — the gateway applies its own default).
|
||||
*
|
||||
* Pure string-prefix classification; does not validate the id is a real model.
|
||||
*/
|
||||
export function inferProviderId(modelId: string): string {
|
||||
if (typeof modelId !== 'string' || modelId.length === 0) return DEFAULT_CLINE_PROVIDER_ID;
|
||||
const lower = modelId.toLowerCase();
|
||||
if (lower.startsWith('claude')) return 'anthropic';
|
||||
if (lower.startsWith('gpt') || lower.startsWith('o1') || lower.startsWith('o3') || lower.startsWith('o4')) return 'openai';
|
||||
if (lower.startsWith('gemini')) return 'google';
|
||||
if (lower.startsWith('deepseek')) return 'deepseek';
|
||||
return DEFAULT_CLINE_PROVIDER_ID;
|
||||
}
|
||||
|
||||
type ModelOverrideMap = Record<string, string> | null | undefined;
|
||||
type ProfileOverrideMap = Record<string, Record<string, string>> | null | undefined;
|
||||
|
||||
type AgentModelParams = { providerId: string; modelId: string };
|
||||
|
||||
/**
|
||||
* Resolve the createAgentModel call params for a cline subagent from MSD's model
|
||||
* override config. Mirrors the precedence OpenCode/Codex use (passive hosts
|
||||
* embed the resolved model into agent frontmatter); for cline's active model
|
||||
* mode the same resolution flows to `gateway.createAgentModel(params)`.
|
||||
*
|
||||
* Precedence (matches MSD's model_overrides > model_profile_overrides contract):
|
||||
* 1. `modelOverrides[agentType]` — direct per-agent override
|
||||
* 2. `modelProfileOverrides[profile][agentType]` — profile-scoped override
|
||||
* 3. null — no override; the host gateway applies its own default
|
||||
*
|
||||
* Pure; never throws. Non-string / empty override values are ignored (fail-safe).
|
||||
*
|
||||
* @returns the `{providerId, modelId}` for createAgentModel, or null when no
|
||||
* override is configured (the gateway default applies — MSD does NOT
|
||||
* call createAgentModel in that case).
|
||||
*/
|
||||
export function resolveClineAgentModelParams(args: {
|
||||
agentType: string;
|
||||
modelOverrides?: ModelOverrideMap;
|
||||
modelProfileOverrides?: ProfileOverrideMap;
|
||||
profile?: string;
|
||||
}): AgentModelParams | null {
|
||||
const { agentType, modelOverrides, modelProfileOverrides, profile } = args;
|
||||
if (!agentType || typeof agentType !== 'string') return null;
|
||||
|
||||
// 1. Direct per-agent override wins.
|
||||
if (modelOverrides && typeof modelOverrides === 'object') {
|
||||
const direct = modelOverrides[agentType];
|
||||
if (typeof direct === 'string' && direct.length > 0) {
|
||||
return { providerId: inferProviderId(direct), modelId: direct };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Profile-scoped override.
|
||||
if (modelProfileOverrides && typeof modelProfileOverrides === 'object' && profile) {
|
||||
const profileEntry = modelProfileOverrides[profile];
|
||||
if (profileEntry && typeof profileEntry === 'object') {
|
||||
const profileModel = profileEntry[agentType];
|
||||
if (typeof profileModel === 'string' && profileModel.length > 0) {
|
||||
return { providerId: inferProviderId(profileModel), modelId: profileModel };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. No override — gateway default applies.
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user