diff --git a/.eca-plugin/marketplace.json b/.eca-plugin/marketplace.json
index 43ed58f..eb6b7df 100644
--- a/.eca-plugin/marketplace.json
+++ b/.eca-plugin/marketplace.json
@@ -160,6 +160,16 @@
"icon": "πͺ¨",
"featured": false
},
+ {
+ "name": "subteam",
+ "description": "Coordinator-led feature workflow with prefixed planning, critique, implementation, and validation subagents plus hook-assisted rendezvous",
+ "source": "plugins/subteam",
+ "category": "Workflow",
+ "tags": ["subagent", "workflow", "planning", "validation", "hooks"],
+ "author": "editor-code-assistant",
+ "icon": "π§βπ€βπ§",
+ "featured": false
+ },
{
"name": "allium",
"description": "Durable behavioural intent for ECA: capture what software is meant to do, surface ambiguity, spot spec-code drift, and generate tests; optional Allium CLI adds validation and analysis",
diff --git a/plugins/context-mode/.mcp.json b/plugins/context-mode/.mcp.json
index a008bdc..1d40040 100644
--- a/plugins/context-mode/.mcp.json
+++ b/plugins/context-mode/.mcp.json
@@ -1,7 +1,10 @@
{
"mcpServers": {
"context-mode": {
- "command": "${plugin:root}/hooks/ctx-server.sh"
+ "command": "${plugin:root}/hooks/ctx-server.sh",
+ "env": {
+ "CONTEXT_MODE_PLATFORM": "eca"
+ }
}
}
}
diff --git a/plugins/context-mode/README.md b/plugins/context-mode/README.md
index f29dea0..7e32a84 100644
--- a/plugins/context-mode/README.md
+++ b/plugins/context-mode/README.md
@@ -1,17 +1,19 @@
# context-mode
-> ECA plugin for [context-mode](https://github.com/mksglu/context-mode) β saves up to 98% of your context window by routing large output through a sandboxed execution engine with BM25-indexed search and session continuity across compactions.
+> ECA plugin for [context-mode](https://github.com/mksglu/context-mode) β saves context by routing large output through sandboxed execution, BM25-indexed search, and session continuity hooks.
## What it does
-- **Sandboxed execution** β `ctx_execute` and `ctx_execute_file` run code in an isolated subprocess; only `console.log()` output enters context
-- **FTS5 knowledge base** β `ctx_batch_execute` auto-indexes all command output; `ctx_search` retrieves only the relevant chunks
-- **Session continuity** β every file edit, git operation, decision, and error is captured to SQLite; after compaction the agent resumes with full context restored
-- **Routing enforcement** β a `preToolCall` hook intercepts `eca__shell_command` / `eca__read_file` / `eca__grep` and nudges the agent toward sandbox tools; `curl`/`wget` are blocked
+- **Sandboxed execution** β `ctx_execute` and `ctx_execute_file` run code in an isolated subprocess; only intentional stdout enters context
+- **FTS5 knowledge base** β `ctx_batch_execute` auto-indexes command output; `ctx_search` retrieves only relevant chunks
+- **Session continuity** β ECA hooks capture prompts/tool calls and preserve resume snapshots across compaction through this plugin's local adapter bridge
+- **Routing policy** β rules and skills steer large shell/read/grep work toward sandbox tools; the `preToolCall` hook denies direct `curl`/`wget`-style fetches through `hooks/ctx-eca-adapter.mjs`
## Prerequisites
-Install the `context-mode` npm package. With [mise](https://mise.jdx.dev/):
+Install the `context-mode` npm package globally. The plugin supplies its own ECA hook adapter bridge, so it does not require an upstream ECA hook dispatcher.
+
+With [mise](https://mise.jdx.dev/):
```toml
# ~/.config/mise/config.toml
@@ -25,6 +27,8 @@ Or globally:
npm install -g context-mode
```
+The MCP server is started through `hooks/ctx-server.sh` so the package can keep its sandbox runtime setup intact.
+
## Commands
| Command | Description |
@@ -41,7 +45,7 @@ npm install -g context-mode
| `context-mode__ctx_execute` | Run code in a sandbox (JS, TS, Python, Shell, Ruby, Go, Rust, Perl) |
| `context-mode__ctx_execute_file` | Load a file into `FILE_CONTENT` and analyze it in a sandbox |
| `context-mode__ctx_batch_execute` | Run multiple commands, auto-index output, search results β one call |
-| `context-mode__ctx_index` | Store content in the FTS5 knowledge base |
+| `context-mode__ctx_index` | Store file/directory content in the FTS5 knowledge base |
| `context-mode__ctx_search` | BM25 search across indexed content |
| `context-mode__ctx_fetch_and_index` | Fetch a URL, convert HTML to markdown, index for search |
| `context-mode__ctx_stats` | Context savings breakdown |
@@ -49,27 +53,40 @@ npm install -g context-mode
| `context-mode__ctx_upgrade` | Upgrade context-mode in place |
| `context-mode__ctx_purge` | Wipe the indexed knowledge base |
+## ECA hooks
+
+This plugin registers ECA-native hooks:
+
+- `chatStart` β inject routing guidance and resume context
+- `preRequest` β capture prompts and add lightweight guidance
+- `preToolCall` β deny direct context-flooding fetches and route large work
+- `postToolCall` β capture successful and failed tool results for continuity
+- `preCompact` β persist a resume snapshot before compaction
+- `postCompact` β inject the saved snapshot after compaction
+
+ECA `sessionStart` output is not used for model context; startup context belongs in `chatStart`.
+
## Usage
-Once installed, context-mode works automatically. The routing rules guide the agent toward sandbox tools for any operation that produces large output.
+Once installed, the MCP server works through `.mcp.json` and hook continuity works through `hooks/ctx-eca-adapter.mjs`. The rules guide the agent toward sandbox tools for operations that may produce large output.
**Check everything is working:**
-```
+```text
ctx doctor
```
**See context savings:**
-```
+```text
ctx stats
```
-**After compaction:** the agent automatically resumes from the session state captured in SQLite β no manual intervention needed.
+**After compaction:** `preCompact` saves a snapshot and `postCompact` injects it into ECA's compact summary, so the agent can continue with restored session context.
## Session continuity
-Every significant tool call is tracked:
+Session history is searchable:
| Need | Command |
|------|---------|
@@ -86,6 +103,6 @@ The core paradigm: write code that processes data and `console.log()` only the a
context-mode__ctx_execute("javascript", `
const { execSync } = await import('node:child_process');
const prs = JSON.parse(execSync('gh pr list --json number,title,state --limit 20').toString());
- prs.forEach(p => console.log(\`#\${p.number} [\${p.state}] \${p.title}\`));
+ prs.forEach(p => console.log('#' + p.number + ' [' + p.state + '] ' + p.title));
`)
```
diff --git a/plugins/context-mode/hooks/ctx-chatstart.sh b/plugins/context-mode/hooks/ctx-chatstart.sh
new file mode 100755
index 0000000..9124614
--- /dev/null
+++ b/plugins/context-mode/hooks/ctx-chatstart.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v context-mode &>/dev/null; then
+ exit 0
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec node "$SCRIPT_DIR/ctx-eca-adapter.mjs" chatstart
diff --git a/plugins/context-mode/hooks/ctx-eca-adapter.mjs b/plugins/context-mode/hooks/ctx-eca-adapter.mjs
new file mode 100755
index 0000000..235889c
--- /dev/null
+++ b/plugins/context-mode/hooks/ctx-eca-adapter.mjs
@@ -0,0 +1,481 @@
+#!/usr/bin/env node
+import { existsSync, statSync } from 'node:fs';
+import { dirname, join, resolve } from 'node:path';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const ECA_OPTS = {
+ configDir: '.config/eca',
+ configDirEnv: 'ECA_CONFIG_DIR',
+ projectDirEnv: undefined,
+ sessionIdEnv: undefined,
+};
+
+const REQUIRED_ROOT_FILES = [
+ 'hooks/core/routing.mjs',
+ 'hooks/session-helpers.mjs',
+ 'hooks/session-loaders.mjs',
+ 'hooks/routing-block.mjs',
+];
+
+const EVENT = (process.argv[2] || '').toLowerCase();
+
+function debug(message) {
+ if (process.env.CONTEXT_MODE_ECA_DEBUG === '1') {
+ process.stderr.write(`[context-mode eca] ${message}\n`);
+ }
+}
+
+function emitJson(value = {}) {
+ process.stdout.write(`${JSON.stringify(value)}\n`);
+}
+
+function noOutput() {
+ // ECA treats empty stdout on exit 0 as no hook output.
+}
+
+function fileExists(path) {
+ try {
+ return existsSync(path) && statSync(path).isFile();
+ } catch {
+ return false;
+ }
+}
+
+function dirExists(path) {
+ try {
+ return existsSync(path) && statSync(path).isDirectory();
+ } catch {
+ return false;
+ }
+}
+
+function isValidRoot(root) {
+ return Boolean(root) && REQUIRED_ROOT_FILES.every((file) => fileExists(join(root, file)));
+}
+
+function commandOutput(command, args = []) {
+ try {
+ return execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
+ } catch {
+ return '';
+ }
+}
+
+function which(command) {
+ return commandOutput('bash', ['-lc', `command -v ${JSON.stringify(command)}`]);
+}
+
+function candidateRootsFromBin(binPath) {
+ if (!binPath) return [];
+ const binDir = dirname(binPath);
+ return [
+ resolve(binDir, '..', 'lib', 'node_modules', 'context-mode'),
+ resolve(binDir, '..', 'share', 'node_modules', 'context-mode'),
+ resolve(binDir, '..'),
+ resolve(binDir, '..', '..'),
+ ];
+}
+
+function resolveContextModeRoot() {
+ const envRoot = process.env.CONTEXT_MODE_ROOT;
+ if (isValidRoot(envRoot)) return resolve(envRoot);
+
+ const directBin = which('context-mode');
+ for (const root of candidateRootsFromBin(directBin)) {
+ if (isValidRoot(root)) return root;
+ }
+
+ if (which('mise')) {
+ const miseBin = commandOutput('mise', ['which', 'context-mode']);
+ for (const root of candidateRootsFromBin(miseBin)) {
+ if (isValidRoot(root)) return root;
+ }
+ }
+
+ return null;
+}
+
+async function importFromRoot(root, relativePath) {
+ return await import(pathToFileURL(join(root, relativePath)).href);
+}
+
+async function readHookInput(mods) {
+ const raw = await mods.helpers.readStdin();
+ return mods.helpers.parseStdin(raw);
+}
+
+function ecaSessionId(input) {
+ return input?.chat_id || input?.session_id || input?.conversation_id || `pid-${process.ppid}`;
+}
+
+function ecaProjectDir(input) {
+ if (typeof input?.cwd === 'string' && input.cwd.length > 0) return input.cwd;
+ if (Array.isArray(input?.workspaces) && input.workspaces.length > 0) {
+ const first = input.workspaces[0];
+ if (typeof first === 'string') return first;
+ if (typeof first?.path === 'string') return first.path;
+ if (typeof first?.uri === 'string') {
+ try { return fileURLToPath(first.uri); } catch {}
+ }
+ }
+ return process.cwd();
+}
+
+function normalizedInput(input) {
+ return {
+ ...input,
+ session_id: ecaSessionId(input),
+ cwd: ecaProjectDir(input),
+ };
+}
+
+function ecaToolNamer(tool) {
+ return `context-mode__${tool}`;
+}
+
+const CONTEXT_MODE_SERVER = 'context-mode';
+const ECA_CTX_PREFIX = `${CONTEXT_MODE_SERVER}__ctx_`;
+
+function rewriteToolNames(text = '') {
+ return String(text)
+ .replace(new RegExp(`\\bmcp__${CONTEXT_MODE_SERVER}__ctx_`, 'g'), ECA_CTX_PREFIX)
+ .replace(/\bctx_(execute_file|batch_execute|execute|fetch_and_index|search|stats|doctor|upgrade|purge|index)\b/g, `${ECA_CTX_PREFIX}$1`)
+ .replace(new RegExp(`\\b${ECA_CTX_PREFIX}${CONTEXT_MODE_SERVER}__`, 'g'), `${CONTEXT_MODE_SERVER}__`);
+}
+
+function ecaFullToolName(input) {
+ const server = input?.server;
+ const tool = input?.tool_name || '';
+ if (server && server !== 'eca') return `${server}__${tool}`;
+ return tool;
+}
+
+function canonicalToolName(input) {
+ const full = ecaFullToolName(input);
+ if (full.startsWith('context-mode__ctx_')) return full;
+ if (input?.server && input.server !== 'eca') return `mcp__${input.server}__${input.tool_name}`;
+
+ const aliases = {
+ shell_command: 'Bash',
+ read_file: 'Read',
+ write_file: 'Write',
+ edit_file: 'Edit',
+ move_file: 'Edit',
+ xml_edit: 'Edit',
+ grep: 'Grep',
+ directory_tree: 'Glob',
+ spawn_agent: 'Agent',
+ ask_user: 'AskUserQuestion',
+ git: 'Bash',
+ task: 'TodoWrite',
+ };
+ return aliases[full] || full;
+}
+
+function normalizeToolInput(toolName, toolInput = {}) {
+ if (!toolInput || typeof toolInput !== 'object') return {};
+ const out = { ...toolInput };
+ if (toolName === 'read_file' && out.path) out.file_path = out.path;
+ if (toolName === 'write_file' && out.path) out.file_path = out.path;
+ if (toolName === 'edit_file') {
+ if (out.path) out.file_path = out.path;
+ if (out.original_content !== undefined) out.old_string = out.original_content;
+ if (out.new_content !== undefined) out.new_string = out.new_content;
+ }
+ if (toolName === 'move_file') {
+ out.file_path = out.source;
+ out.destination_path = out.destination;
+ }
+ if (toolName === 'spawn_agent') {
+ out.prompt = out.task || out.prompt || '';
+ out.subagent_type = out.agent || out.subagent_type || 'general-purpose';
+ }
+ if (toolName === 'git') {
+ out.command = `git ${out.operation || ''}`.trim();
+ }
+ return out;
+}
+
+function denormalizeUpdatedInput(originalToolName, updatedInput) {
+ if (!updatedInput || typeof updatedInput !== 'object') return updatedInput;
+ if (originalToolName === 'edit_file') {
+ const out = { ...updatedInput };
+ if (out.file_path && !out.path) out.path = out.file_path;
+ if (out.old_string !== undefined && out.original_content === undefined) out.original_content = out.old_string;
+ if (out.new_string !== undefined && out.new_content === undefined) out.new_content = out.new_string;
+ delete out.file_path;
+ delete out.old_string;
+ delete out.new_string;
+ return out;
+ }
+ if ((originalToolName === 'read_file' || originalToolName === 'write_file') && updatedInput.file_path) {
+ const out = { ...updatedInput, path: updatedInput.path || updatedInput.file_path };
+ delete out.file_path;
+ return out;
+ }
+ return updatedInput;
+}
+
+function stringifyToolResponse(response, error) {
+ if (error) return typeof error === 'string' ? error : JSON.stringify(error);
+ if (typeof response === 'string') return response;
+ if (response == null) return '';
+ try { return JSON.stringify(response); } catch { return String(response); }
+}
+
+function fallbackNetworkDecision(input) {
+ if (canonicalToolName(input) !== 'Bash') return null;
+ const command = String(input?.tool_input?.command || '');
+ if (!/(^|\s|&&|\|\||;)(curl|wget)\s/i.test(command)) return null;
+ if (/\s(-o|--output|-O|--output-document)\s+\S+/.test(command) && /\s(-s|--silent|-q|--quiet)\b/.test(command)) return null;
+ return {
+ action: 'deny',
+ reason: 'context-mode: direct curl/wget output would flood context. Use context-mode__ctx_fetch_and_index(url, source) then context-mode__ctx_search(queries), or context-mode__ctx_execute(language, code) to fetch and print only derived results.',
+ };
+}
+
+function ecaEventInput(input) {
+ const toolName = input?.tool_name || '';
+ const normalizedToolInput = normalizeToolInput(toolName, input?.tool_input || {});
+ return {
+ ...normalizedInput(input),
+ tool_name: canonicalToolName(input),
+ tool_input: normalizedToolInput,
+ tool_response: stringifyToolResponse(input?.tool_response, input?.error),
+ tool_output: { isError: Boolean(input?.error) },
+ };
+}
+
+function customEvents(input) {
+ const events = [];
+ const tool = input?.tool_name;
+ const toolInput = input?.tool_input || {};
+
+ if (tool === 'move_file') {
+ events.push({ type: 'file_edit', category: 'file', priority: 2, data: `${toolInput.source || ''} -> ${toolInput.destination || ''}` });
+ }
+ if (tool === 'git') {
+ const op = toolInput.operation || 'git';
+ const summary = toolInput.summary ? `: ${toolInput.summary}` : '';
+ events.push({ type: op === 'commit' ? 'git_commit' : 'git', category: 'git', priority: 2, data: `${op}${summary}` });
+ }
+ if (tool === 'task') {
+ const op = toolInput.op || 'task';
+ const data = JSON.stringify({ operation: op, ids: toolInput.ids, id: toolInput.id, subject: toolInput.task?.subject });
+ events.push({ type: op === 'complete' ? 'task_update' : 'task', category: 'task', priority: 2, data });
+ }
+
+ return events;
+}
+
+async function openSession(mods, input) {
+ const projectDir = ecaProjectDir(input);
+ const sessionId = ecaSessionId(input);
+ const dbPath = mods.helpers.getSessionDBPath(ECA_OPTS, projectDir);
+ const { SessionDB } = await mods.loaders.loadSessionDB();
+ const db = new SessionDB({ dbPath });
+ db.ensureSession(sessionId, projectDir);
+ return { db, sessionId, projectDir };
+}
+
+async function insertEvents(mods, db, sessionId, events, input, projectDir, hookName) {
+ if (!events || events.length === 0) return;
+ try {
+ const attribution = await mods.loaders.loadProjectAttribution();
+ const resolver = attribution.resolveProjectAttributions || attribution.default?.resolveProjectAttributions;
+ if (resolver) {
+ mods.sessionLoaders.attributeAndInsertEvents(db, sessionId, events, normalizedInput(input), projectDir, hookName, resolver);
+ return;
+ }
+ } catch (err) {
+ debug(`attribution failed: ${err?.message || err}`);
+ }
+
+ if (typeof db.bulkInsertEvents === 'function') {
+ db.bulkInsertEvents(sessionId, events, hookName);
+ } else {
+ for (const event of events) db.insertEvent(sessionId, event, hookName);
+ }
+}
+
+async function handleChatStart(mods, input) {
+ let additionalContext = mods.routingBlock.createRoutingBlock(ecaToolNamer);
+ let db;
+ try {
+ const session = await openSession(mods, input);
+ db = session.db;
+ if (input?.resumed) {
+ const events = mods.sessionDirective.getSessionEvents(db, session.sessionId);
+ if (events.length > 0) {
+ const eventsPath = mods.helpers.getSessionEventsPath(ECA_OPTS, session.projectDir);
+ const meta = mods.sessionDirective.writeSessionEventsFile(events, eventsPath);
+ additionalContext += mods.sessionDirective.buildSessionDirective('continue', meta, ecaToolNamer);
+ }
+ }
+ } catch (err) {
+ debug(`chatstart db failed: ${err?.message || err}`);
+ } finally {
+ try { db?.close?.(); } catch {}
+ }
+ emitJson({ additionalContext: rewriteToolNames(additionalContext) });
+}
+
+async function handlePreRequest(mods, input) {
+ let db;
+ try {
+ const session = await openSession(mods, input);
+ db = session.db;
+ const extract = await mods.loaders.loadExtract();
+ const events = extract.extractUserEvents(input?.prompt || '');
+ await insertEvents(mods, db, session.sessionId, events, input, session.projectDir, 'preRequest');
+ } catch (err) {
+ debug(`prerequest failed: ${err?.message || err}`);
+ } finally {
+ try { db?.close?.(); } catch {}
+ }
+ noOutput();
+}
+
+async function handlePreToolUse(mods, input, root) {
+ try {
+ await mods.routing.initSecurity(join(root, 'build'));
+ } catch (err) {
+ debug(`security init failed: ${err?.message || err}`);
+ }
+
+ const normalizedToolInput = normalizeToolInput(input?.tool_name || '', input?.tool_input || {});
+ let decision = null;
+ try {
+ decision = mods.routing.routePreToolUse(canonicalToolName(input), normalizedToolInput, ecaProjectDir(input), 'codex', ecaSessionId(input));
+ } catch (err) {
+ debug(`routing failed: ${err?.message || err}`);
+ }
+ decision ??= fallbackNetworkDecision(input);
+
+ if (!decision) return emitJson({});
+
+ if (decision.action === 'deny') {
+ return emitJson({ approval: 'deny', additionalContext: rewriteToolNames(decision.reason || '') });
+ }
+ if (decision.action === 'ask') return emitJson({ approval: 'ask' });
+ if (decision.action === 'modify') {
+ const updatedInput = denormalizeUpdatedInput(input?.tool_name || '', decision.updatedInput || {});
+ const reason = rewriteToolNames(updatedInput?.command || decision.reason || '');
+ if (input?.tool_name === 'shell_command' && updatedInput?.command) updatedInput.command = reason;
+ return emitJson({ approval: 'deny', additionalContext: reason || 'context-mode routed this call to context-mode tools.' });
+ }
+ return emitJson({});
+}
+
+async function handlePostToolUse(mods, input) {
+ let db;
+ try {
+ const session = await openSession(mods, input);
+ db = session.db;
+ const extract = await mods.loaders.loadExtract();
+ const events = [
+ ...extract.extractEvents(ecaEventInput(input)),
+ ...customEvents(input),
+ ];
+ await insertEvents(mods, db, session.sessionId, events, input, session.projectDir, 'postToolCall');
+ } catch (err) {
+ debug(`posttooluse failed: ${err?.message || err}`);
+ } finally {
+ try { db?.close?.(); } catch {}
+ }
+ noOutput();
+}
+
+async function handlePreCompact(mods, input) {
+ let db;
+ try {
+ const session = await openSession(mods, input);
+ db = session.db;
+ const events = mods.sessionDirective.getSessionEvents(db, session.sessionId);
+ const snapshotMod = await mods.loaders.loadSnapshot();
+ const stats = db.getSessionStats(session.sessionId);
+ const snapshot = snapshotMod.buildResumeSnapshot(events, {
+ compactCount: (stats?.compact_count || 0) + 1,
+ searchTool: 'context-mode__ctx_search',
+ });
+ db.upsertResume(session.sessionId, rewriteToolNames(snapshot), events.length);
+ db.incrementCompactCount(session.sessionId);
+ } catch (err) {
+ debug(`precompact failed: ${err?.message || err}`);
+ } finally {
+ try { db?.close?.(); } catch {}
+ }
+ emitJson({});
+}
+
+async function handlePostCompact(mods, input) {
+ let db;
+ try {
+ const session = await openSession(mods, input);
+ db = session.db;
+ const resume = db.getResume(session.sessionId);
+ if (resume && !resume.consumed) {
+ db.markResumeConsumed(session.sessionId);
+ return emitJson({ additionalContext: rewriteToolNames(resume.snapshot || '') });
+ }
+ } catch (err) {
+ debug(`postcompact failed: ${err?.message || err}`);
+ } finally {
+ try { db?.close?.(); } catch {}
+ }
+ emitJson({});
+}
+
+async function loadModules(root) {
+ const helpers = await importFromRoot(root, 'hooks/session-helpers.mjs');
+ const sessionLoaders = await importFromRoot(root, 'hooks/session-loaders.mjs');
+ return {
+ helpers,
+ sessionLoaders,
+ loaders: sessionLoaders.createSessionLoaders(join(root, 'hooks')),
+ routing: await importFromRoot(root, 'hooks/core/routing.mjs'),
+ routingBlock: await importFromRoot(root, 'hooks/routing-block.mjs'),
+ sessionDirective: await importFromRoot(root, 'hooks/session-directive.mjs'),
+ };
+}
+
+async function main() {
+ const root = resolveContextModeRoot();
+ if (!root) {
+ debug('context-mode package root not found; no-op');
+ return;
+ }
+
+ let mods;
+ try {
+ mods = await loadModules(root);
+ } catch (err) {
+ debug(`module load failed: ${err?.message || err}`);
+ return;
+ }
+
+ let input;
+ try {
+ input = await readHookInput(mods);
+ } catch (err) {
+ debug(`stdin parse failed: ${err?.message || err}`);
+ return;
+ }
+
+ switch (EVENT) {
+ case 'chatstart': return await handleChatStart(mods, normalizedInput(input));
+ case 'prerequest': return await handlePreRequest(mods, normalizedInput(input));
+ case 'pretooluse': return await handlePreToolUse(mods, normalizedInput(input), root);
+ case 'posttooluse': return await handlePostToolUse(mods, normalizedInput(input));
+ case 'precompact': return await handlePreCompact(mods, normalizedInput(input));
+ case 'postcompact': return await handlePostCompact(mods, normalizedInput(input));
+ default:
+ debug(`unknown event: ${EVENT}`);
+ return;
+ }
+}
+
+main().catch((err) => {
+ debug(`unhandled failure: ${err?.stack || err}`);
+});
diff --git a/plugins/context-mode/hooks/ctx-postcompact.sh b/plugins/context-mode/hooks/ctx-postcompact.sh
new file mode 100755
index 0000000..3046220
--- /dev/null
+++ b/plugins/context-mode/hooks/ctx-postcompact.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v context-mode &>/dev/null; then
+ exit 0
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec node "$SCRIPT_DIR/ctx-eca-adapter.mjs" postcompact
diff --git a/plugins/context-mode/hooks/ctx-posttooluse.sh b/plugins/context-mode/hooks/ctx-posttooluse.sh
index f6048ad..db64488 100755
--- a/plugins/context-mode/hooks/ctx-posttooluse.sh
+++ b/plugins/context-mode/hooks/ctx-posttooluse.sh
@@ -1,20 +1,9 @@
#!/usr/bin/env bash
-# context-mode postToolCall hook for ECA.
-# Captures tool events (files modified, decisions, errors, git ops, tasksβ¦)
-# to a per-project SQLite database for session continuity after compaction.
set -euo pipefail
if ! command -v context-mode &>/dev/null; then
exit 0
fi
-CM_BIN="$(command -v context-mode)"
-if command -v mise &>/dev/null; then
- CM_BIN="$(mise which context-mode 2>/dev/null || echo "$CM_BIN")"
-fi
-
-export CLAUDE_PROJECT_DIR="${workspaceFolder:-$(pwd)}"
-export CONTEXT_MODE_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
-
-CM_ROOT="$(dirname "$CM_BIN")/../lib/node_modules/context-mode"
-exec node "$CM_ROOT/hooks/posttooluse.mjs"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec node "$SCRIPT_DIR/ctx-eca-adapter.mjs" posttooluse
diff --git a/plugins/context-mode/hooks/ctx-precompact.sh b/plugins/context-mode/hooks/ctx-precompact.sh
new file mode 100755
index 0000000..1812207
--- /dev/null
+++ b/plugins/context-mode/hooks/ctx-precompact.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v context-mode &>/dev/null; then
+ exit 0
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec node "$SCRIPT_DIR/ctx-eca-adapter.mjs" precompact
diff --git a/plugins/context-mode/hooks/ctx-prerequest.sh b/plugins/context-mode/hooks/ctx-prerequest.sh
new file mode 100755
index 0000000..7c40720
--- /dev/null
+++ b/plugins/context-mode/hooks/ctx-prerequest.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v context-mode &>/dev/null; then
+ exit 0
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec node "$SCRIPT_DIR/ctx-eca-adapter.mjs" prerequest
diff --git a/plugins/context-mode/hooks/ctx-pretooluse.sh b/plugins/context-mode/hooks/ctx-pretooluse.sh
index e0d80b5..a28f17d 100755
--- a/plugins/context-mode/hooks/ctx-pretooluse.sh
+++ b/plugins/context-mode/hooks/ctx-pretooluse.sh
@@ -1,20 +1,9 @@
#!/usr/bin/env bash
-# context-mode preToolCall hook for ECA.
-# Intercepts eca__shell_command, eca__read_file, and eca__grep; blocks
-# curl/wget; injects routing nudges toward sandbox tools.
set -euo pipefail
if ! command -v context-mode &>/dev/null; then
exit 0
fi
-CM_BIN="$(command -v context-mode)"
-if command -v mise &>/dev/null; then
- CM_BIN="$(mise which context-mode 2>/dev/null || echo "$CM_BIN")"
-fi
-
-export CLAUDE_PROJECT_DIR="${workspaceFolder:-$(pwd)}"
-export CONTEXT_MODE_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
-
-CM_ROOT="$(dirname "$CM_BIN")/../lib/node_modules/context-mode"
-exec node "$CM_ROOT/hooks/pretooluse.mjs"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+exec node "$SCRIPT_DIR/ctx-eca-adapter.mjs" pretooluse
diff --git a/plugins/context-mode/hooks/ctx-sessionstart.sh b/plugins/context-mode/hooks/ctx-sessionstart.sh
deleted file mode 100755
index 7178bc2..0000000
--- a/plugins/context-mode/hooks/ctx-sessionstart.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-# context-mode sessionStart hook for ECA.
-# Injects routing instructions at session start; rebuilds session state after
-# compaction from the SQLite snapshot captured by ctx-posttooluse.sh.
-set -euo pipefail
-
-if ! command -v context-mode &>/dev/null; then
- exit 0
-fi
-
-CM_BIN="$(command -v context-mode)"
-if command -v mise &>/dev/null; then
- CM_BIN="$(mise which context-mode 2>/dev/null || echo "$CM_BIN")"
-fi
-
-export CLAUDE_PROJECT_DIR="${workspaceFolder:-$(pwd)}"
-export CONTEXT_MODE_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
-
-CM_ROOT="$(dirname "$CM_BIN")/../lib/node_modules/context-mode"
-exec node "$CM_ROOT/hooks/sessionstart.mjs"
diff --git a/plugins/context-mode/hooks/hooks.json b/plugins/context-mode/hooks/hooks.json
index 6fd4300..10db428 100644
--- a/plugins/context-mode/hooks/hooks.json
+++ b/plugins/context-mode/hooks/hooks.json
@@ -1,25 +1,75 @@
{
- "context-mode.session-start": {
- "type": "sessionStart",
+ "context-mode.chat-start": {
+ "type": "chatStart",
"visible": false,
+ "description": "Inject context-mode routing guidance and resume context at chat start.",
"actions": [
- { "type": "shell", "file": "ctx-sessionstart.sh" }
+ { "type": "shell", "file": "${plugin:root}/hooks/ctx-chatstart.sh" }
+ ]
+ },
+ "context-mode.pre-request": {
+ "type": "preRequest",
+ "visible": false,
+ "description": "Capture user prompts and provide lightweight context-mode request guidance.",
+ "actions": [
+ { "type": "shell", "file": "${plugin:root}/hooks/ctx-prerequest.sh" }
]
},
"context-mode.pre-tool": {
"type": "preToolCall",
- "matcher": "eca__shell_command|eca__read_file|eca__grep",
+ "matcher": {
+ "eca__shell_command": {},
+ "eca__read_file": {},
+ "eca__grep": {},
+ "eca__edit_file": {},
+ "eca__write_file": {},
+ "eca__move_file": {},
+ "eca__directory_tree": {},
+ "eca__spawn_agent": {},
+ "context-mode": {}
+ },
"visible": false,
+ "description": "Block context-flooding shell fetches and route large work through context-mode.",
"actions": [
- { "type": "shell", "file": "ctx-pretooluse.sh" }
+ { "type": "shell", "file": "${plugin:root}/hooks/ctx-pretooluse.sh" }
]
},
"context-mode.post-tool": {
"type": "postToolCall",
- "matcher": "eca__shell_command|eca__read_file|eca__write_file|eca__edit_file|eca__grep|eca__directory_tree|eca__git|eca__spawn_agent|eca__task|eca__compact_chat|context-mode",
+ "matcher": {
+ "eca__shell_command": {},
+ "eca__read_file": {},
+ "eca__write_file": {},
+ "eca__edit_file": {},
+ "eca__move_file": {},
+ "eca__grep": {},
+ "eca__directory_tree": {},
+ "eca__git": {},
+ "eca__spawn_agent": {},
+ "eca__task": {},
+ "context-mode": {}
+ },
+ "runOnError": true,
+ "visible": false,
+ "description": "Capture tool results for context-mode session continuity.",
+ "actions": [
+ { "type": "shell", "file": "${plugin:root}/hooks/ctx-posttooluse.sh" }
+ ]
+ },
+ "context-mode.pre-compact": {
+ "type": "preCompact",
+ "visible": false,
+ "description": "Persist context-mode resume snapshot before ECA compacts the chat.",
+ "actions": [
+ { "type": "shell", "file": "${plugin:root}/hooks/ctx-precompact.sh" }
+ ]
+ },
+ "context-mode.post-compact": {
+ "type": "postCompact",
"visible": false,
+ "description": "Inject context-mode resume snapshot after ECA compacts the chat.",
"actions": [
- { "type": "shell", "file": "ctx-posttooluse.sh" }
+ { "type": "shell", "file": "${plugin:root}/hooks/ctx-postcompact.sh" }
]
}
}
diff --git a/plugins/context-mode/rules/context-mode.md b/plugins/context-mode/rules/context-mode.md
index f9f7128..f68f867 100644
--- a/plugins/context-mode/rules/context-mode.md
+++ b/plugins/context-mode/rules/context-mode.md
@@ -1,57 +1,64 @@
-# context-mode β MANDATORY routing rules
+# context-mode β routing rules
-context-mode MCP tools available. Rules protect context window from flooding.
-One unrouted `eca__shell_command` can dump 56 KB into context.
-Hooks enforce routing for `eca__shell_command`, `eca__read_file`, and `eca__grep`.
-Rules cover redirections hooks cannot catch.
+context-mode MCP tools are available as `context-mode__ctx_*`. Use them to
+protect ECA's context window from raw large outputs. One unrouted shell command
+or file read can dump tens of KB into context.
-## Think in Code β MANDATORY
+ECA hooks block dangerous direct fetches through this plugin's local adapter
+bridge. Soft routing remains a standing rule: prefer sandbox/search tools
+whenever output may be large.
+
+## Think in Code
Analyze/count/filter/compare/search/parse/transform data: **write code** via
`context-mode__ctx_execute(language, code)`, `console.log()` only the answer.
Do NOT read raw data into context. PROGRAM the analysis, not COMPUTE it.
Pure JavaScript β Node.js built-ins only (`fs`, `path`, `child_process`).
-`try/catch`, handle `null`/`undefined`. One script replaces ten tool calls.
+Use `try/catch`, handle `null`/`undefined`. One script replaces ten tool calls.
## BLOCKED β do NOT use
-### curl / wget β FORBIDDEN (hook-enforced)
-Do NOT use `curl`/`wget` in `eca__shell_command`. Hook blocks it.
+### curl / wget β forbidden
+Do NOT use `curl`/`wget` in `eca__shell_command`. The ECA hook denies these
+through the plugin-local adapter.
Use: `context-mode__ctx_fetch_and_index(url, source)` or
`context-mode__ctx_execute(language: "javascript", code: "const r = await fetch(...)")`
-### Inline HTTP β FORBIDDEN
+### Inline HTTP β forbidden
No `node -e "fetch(..."`, `python -c "requests.get(..."`. Bypasses sandbox.
Use: `context-mode__ctx_execute(language, code)` β only stdout enters context.
-### Direct web fetching β FORBIDDEN
-Use: `context-mode__ctx_fetch_and_index(url, source)` then `context-mode__ctx_search(queries)`
+### Direct web fetching β forbidden
+Use: `context-mode__ctx_fetch_and_index(url, source)` then `context-mode__ctx_search(queries)`.
-## REDIRECTED β use sandbox
+## REDIRECTED β use sandbox/search
-### eca__shell_command (>20 lines output)
-Use ONLY for: `git add/commit/push/checkout`, `mkdir`, `rm`, `mv`, `cd`, `echo`.
-Otherwise: `context-mode__ctx_batch_execute(commands, queries)` or `context-mode__ctx_execute(language: "shell", code: "...")`
+### `eca__shell_command` (>20 lines output)
+Use directly only for bounded operations: `git add/commit/push/checkout`,
+`mkdir`, `rm`, `mv`, `cd`, `echo`, small status/version checks.
+Otherwise use `context-mode__ctx_batch_execute(commands, queries)` or
+`context-mode__ctx_execute(language: "shell", code: "...")`.
-### eca__read_file (for analysis)
-Reading to **edit** β `eca__read_file` correct.
+### `eca__read_file` (for analysis)
+Reading to **edit** β `eca__read_file` is correct.
Reading to **analyze/explore/summarize** β `context-mode__ctx_execute_file(path, language, code)`.
-### eca__grep (large results)
-Use `context-mode__ctx_execute(language: "shell", code: "grep ...")` in sandbox.
+### `eca__grep` (large results)
+Use `context-mode__ctx_execute(language: "shell", code: "grep ...")` in the sandbox.
## Tool selection
0. **MEMORY**: `context-mode__ctx_search(sort: "timeline")` β after resume, check prior context before asking user.
-1. **GATHER**: `context-mode__ctx_batch_execute(commands, queries)` β ONE call replaces 30+.
-2. **FOLLOW-UP**: `context-mode__ctx_search(queries: ["q1", "q2", ...])` β ONE call.
+1. **GATHER**: `context-mode__ctx_batch_execute(commands, queries)` β one call replaces 30+.
+2. **FOLLOW-UP**: `context-mode__ctx_search(queries: ["q1", "q2", ...])` β one call.
3. **PROCESSING**: `context-mode__ctx_execute(language, code)` | `context-mode__ctx_execute_file(path, language, code)`.
4. **WEB**: `context-mode__ctx_fetch_and_index(url, source)` then `context-mode__ctx_search(queries)`.
-5. **INDEX**: `context-mode__ctx_index(path, source)` β use `path` not `content` for large data.
+5. **INDEX**: `context-mode__ctx_index(path, source)` β use `path` instead of `content` for large data.
-## Session Continuity
+## Session continuity
-On resume, search BEFORE asking the user. DO NOT ask "what were we working on?" β SEARCH FIRST.
+Session history is searchable. On resume, search BEFORE asking the user. Do not
+ask "what were we working on?" until `ctx_search` has failed to recover it.
| Need | Command |
|------|---------|
diff --git a/plugins/context-mode/skills/context-mode/SKILL.md b/plugins/context-mode/skills/context-mode/SKILL.md
index af36416..b218802 100644
--- a/plugins/context-mode/skills/context-mode/SKILL.md
+++ b/plugins/context-mode/skills/context-mode/SKILL.md
@@ -2,103 +2,299 @@
name: context-mode
description: |
Use context-mode tools (context-mode__ctx_execute, context-mode__ctx_execute_file) instead of
- eca__shell_command/eca__read_file when processing large outputs. Triggers: "analyze logs",
- "summarize output", "process data", "parse JSON", "filter results", "extract errors",
- "check build output", "analyze dependencies", "process API response", "large file analysis",
+ eca__shell_command/eca__read_file when processing large outputs. Triggers: "analyze logs", "summarize output", "process data",
+ "parse JSON", "filter results", "extract errors", "check build output",
+ "analyze dependencies", "process API response", "large file analysis",
+ "page snapshot", "browser snapshot", "DOM structure", "inspect page",
+ "accessibility tree", "Playwright snapshot",
"run tests", "test output", "coverage report", "git log", "recent commits",
- "diff between branches", "fetch docs", "API reference", "index documentation",
- "call API", "check response", "query results", "find TODOs", "count lines",
- "codebase statistics", "security audit", "outdated packages", "dependency tree".
- Also triggers on ANY tool output that may exceed 20 lines.
+ "diff between branches", "list containers", "pod status", "disk usage",
+ "fetch docs", "API reference", "index documentation",
+ "call API", "check response", "query results",
+ "find TODOs", "count lines", "codebase statistics", "security audit",
+ "outdated packages", "dependency tree", "cloud resources", "CI/CD output".
+ Also triggers on ANY MCP tool output that may exceed 20 lines.
+ ECA hook routing depends on an installed context-mode version with the ECA adapter.
---
# Context Mode: Default for All Large Output
## MANDATORY RULE
-Default to context-mode for ALL commands that produce output. Only use `eca__shell_command`
-for guaranteed-small-output operations.
+
+
+ Default to context-mode for ALL commands that may produce large output. Only use eca__shell_command for guaranteed-small-output operations.
+
+
`eca__shell_command` whitelist (safe to run directly):
- **File mutations**: `mkdir`, `mv`, `cp`, `rm`, `touch`, `chmod`
- **Git writes**: `git add`, `git commit`, `git push`, `git checkout`, `git branch`, `git merge`
- **Navigation**: `cd`, `pwd`, `which`
- **Process control**: `kill`, `pkill`
-- **Package management**: `npm install`, `pip install`
+- **Package management**: `npm install`, `npm publish`, `pip install`
- **Simple output**: `echo`, `printf`
-**Everything else β `context-mode__ctx_execute` or `context-mode__ctx_execute_file`.**
+**Everything else β `context-mode__ctx_execute` or `context-mode__ctx_execute_file`.** Any command that reads, queries, fetches, lists, logs, tests, builds, diffs, inspects, or calls an external service. This includes ALL CLIs (gh, aws, kubectl, docker, terraform, wrangler, fly, heroku, gcloud, etc.) β there are thousands and we cannot list them all.
-## Think in Code β MANDATORY
+**When uncertain, use context-mode.** Every KB of unnecessary context reduces the quality and speed of the entire session.
-Analyze/count/filter/compare/search/parse/transform data: **write code** via
-`context-mode__ctx_execute(language, code)`, `console.log()` only the answer.
-Do NOT read raw data into context. PROGRAM the analysis, not COMPUTE it.
-Pure JavaScript β Node.js built-ins only (`fs`, `path`, `child_process`).
-`try/catch`, handle `null`/`undefined`. One script replaces ten tool calls.
+## Decision Tree
-## BLOCKED β do NOT use
+```
+About to run a command / read a file / call an API?
+β
+βββ Command is on the eca__shell_command whitelist (file mutations, git writes, navigation, echo)?
+β βββ Use eca__shell_command
+β
+βββ Output MIGHT be large or you're UNSURE?
+β βββ Use context-mode__ctx_execute or context-mode__ctx_execute_file
+β
+βββ Fetching web documentation or HTML page?
+β βββ Use context-mode__ctx_fetch_and_index β context-mode__ctx_search
+β
+βββ Using Playwright (navigate, snapshot, console, network)?
+β βββ ALWAYS use filename parameter to save to file, then:
+β browser_snapshot(filename) β context-mode__ctx_index(path) or context-mode__ctx_execute_file(path)
+β browser_console_messages(filename) β context-mode__ctx_execute_file(path)
+β browser_network_requests(filename) β context-mode__ctx_execute_file(path)
+β β browser_navigate returns a snapshot automatically β ignore it,
+β use browser_snapshot(filename) for any inspection.
+β β Playwright MCP uses a SINGLE browser instance β NOT parallel-safe.
+β For parallel browser ops, use agent-browser via context-mode__ctx_execute instead.
+β
+βββ Using agent-browser (parallel-safe browser automation)?
+β βββ Run via context-mode__ctx_execute(language: "shell") β each call gets its own subprocess:
+β context-mode__ctx_execute(language: "shell", code: "agent-browser open example.com && agent-browser snapshot -i -c")
+β β Supports sessions for isolated browser instances
+β β Safe for parallel subagent execution
+β β Lightweight accessibility tree with ref-based interaction
+β
+βββ Processing output from another MCP tool (Context7, GitHub API, etc.)?
+β βββ Output already in context from a previous tool call?
+β β βββ Use it directly. Do NOT re-index with context-mode__ctx_index(content: ...).
+β βββ Need to search the output multiple times?
+β β βββ Save to file via context-mode__ctx_execute, then context-mode__ctx_index(path) β context-mode__ctx_search
+β βββ One-shot extraction?
+β βββ Save to file via context-mode__ctx_execute, then context-mode__ctx_execute_file(path)
+β
+βββ Reading a file to analyze/summarize (not edit)?
+ βββ Use context-mode__ctx_execute_file (file loads into FILE_CONTENT, not context)
+```
-### curl / wget β FORBIDDEN (hook-enforced)
-Do NOT use `curl`/`wget` in `eca__shell_command`. Dumps raw HTTP into context.
-Use: `context-mode__ctx_fetch_and_index(url, source)` or
- `context-mode__ctx_execute(language: "javascript", code: "const r = await fetch(...)")`
+## When to Use Each Tool
-### Inline HTTP β FORBIDDEN
-No `node -e "fetch(..."`, `python -c "requests.get(..."`. Bypasses sandbox.
-Use: `context-mode__ctx_execute(language, code)` β only stdout enters context.
+| Situation | Tool | Example |
+|-----------|------|---------|
+| Hit an API endpoint | `context-mode__ctx_execute` | `fetch('http://localhost:3000/api/orders')` |
+| Run CLI that returns data | `context-mode__ctx_execute` | `gh pr list`, `aws s3 ls`, `kubectl get pods` |
+| Run tests | `context-mode__ctx_execute` | `npm test`, `pytest`, `go test ./...` |
+| Git operations | `context-mode__ctx_execute` | `git log --oneline -50`, `git diff HEAD~5` |
+| Docker/K8s inspection | `context-mode__ctx_execute` | `docker stats --no-stream`, `kubectl describe pod` |
+| Read a log file | `context-mode__ctx_execute_file` | Parse access.log, error.log, build output |
+| Read a data file | `context-mode__ctx_execute_file` | Analyze CSV, JSON, YAML, XML |
+| Read source code to analyze | `context-mode__ctx_execute_file` | Count functions, find patterns, extract metrics |
+| Fetch web docs | `context-mode__ctx_fetch_and_index` | Index React/Next.js/Zod docs, then search |
+| Playwright snapshot | `browser_snapshot(filename)` β `context-mode__ctx_index(path)` β `context-mode__ctx_search` | Save to file, index server-side, query |
+| Playwright snapshot (one-shot) | `browser_snapshot(filename)` β `context-mode__ctx_execute_file(path)` | Save to file, extract in sandbox |
+| Playwright console/network | `browser_*(filename)` β `context-mode__ctx_execute_file(path)` | Save to file, analyze in sandbox |
+| MCP output (already in context) | Use directly | Don't re-index β it's already loaded |
+| MCP output (need multi-query) | `context-mode__ctx_execute` to save β `context-mode__ctx_index(path)` β `context-mode__ctx_search` | Save to file first, index server-side |
+| Wipe indexed KB content | `context-mode__ctx_purge(confirm: true)` | Permanently deletes all indexed content |
-### Direct web fetching β FORBIDDEN
-Raw HTML can exceed 100 KB.
-Use: `context-mode__ctx_fetch_and_index(url, source)` then `context-mode__ctx_search(queries)`
+## Automatic Triggers
-## Tool Selection
+Use context-mode for ANY of these, without being asked:
-0. **MEMORY**: `context-mode__ctx_search(sort: "timeline")` β after resume, check prior context before asking user.
-1. **GATHER**: `context-mode__ctx_batch_execute(commands, queries)` β runs all commands, auto-indexes, returns search. ONE call replaces 30+.
-2. **FOLLOW-UP**: `context-mode__ctx_search(queries: ["q1", "q2", ...])` β all questions as array, ONE call.
-3. **PROCESSING**: `context-mode__ctx_execute(language, code)` | `context-mode__ctx_execute_file(path, language, code)` β sandbox only.
-4. **WEB**: `context-mode__ctx_fetch_and_index(url, source)` then `context-mode__ctx_search(queries)`.
-5. **INDEX**: `context-mode__ctx_index(content, source)` β store in FTS5 for later search.
+- **API debugging**: "hit this endpoint", "call the API", "check the response", "find the bug in the response"
+- **Log analysis**: "check the logs", "what errors", "read access.log", "debug the 500s"
+- **Test runs**: "run the tests", "check if tests pass", "test suite output"
+- **Git history**: "show recent commits", "git log", "what changed", "diff between branches"
+- **Data inspection**: "look at the CSV", "parse the JSON", "analyze the config"
+- **Infrastructure**: "list containers", "check pods", "S3 buckets", "show running services"
+- **Dependency audit**: "check dependencies", "outdated packages", "security audit"
+- **Build output**: "build the project", "check for warnings", "compile errors"
+- **Code metrics**: "count lines", "find TODOs", "function count", "analyze codebase"
+- **Web docs lookup**: "look up the docs", "check the API reference", "find examples"
-## REDIRECTED β use sandbox
+## Language Selection
-### eca__shell_command (>20 lines output)
-Use ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`.
-Otherwise: `context-mode__ctx_batch_execute(commands, queries)` or
- `context-mode__ctx_execute(language: "shell", code: "...")`
+| Situation | Language | Why |
+|-----------|----------|-----|
+| HTTP/API calls, JSON | `javascript` | Native fetch, JSON.parse, async/await |
+| Data analysis, CSV, stats | `python` | csv, statistics, collections, re |
+| Shell commands with pipes | `shell` | grep, awk, jq, native tools |
+| File pattern matching | `shell` | find, wc, sort, uniq |
-### eca__read_file (for analysis)
-Reading to **edit** β `eca__read_file` correct.
-Reading to **analyze/explore/summarize** β `context-mode__ctx_execute_file(path, language, code)`.
+## Search Query Strategy
-### eca__grep (large results)
-Use `context-mode__ctx_execute(language: "shell", code: "grep ...")` in sandbox.
+- BM25 uses **OR semantics** β results matching more terms rank higher automatically
+- Use 2-4 specific technical terms per query
+- **Always use `source` parameter** when multiple docs are indexed to avoid cross-source contamination
+ - Partial match works: `source: "Node"` matches `"Node.js v22 CHANGELOG"`
+- **Always use `queries` array** β batch ALL search questions in ONE call:
+ - `context-mode__ctx_search(queries: ["transform pipe", "refine superRefine", "coerce codec"], source: "Zod")`
+ - NEVER make multiple separate context-mode__ctx_search() calls β put all queries in one array
-## Output
+## External Documentation
-Terse like caveman. Technical substance exact. Only fluff die.
-Drop: articles, filler, pleasantries, hedging. Fragments OK. Short synonyms. Code unchanged.
-Pattern: [thing] [action] [reason]. [next step].
-Write artifacts to FILES β never inline. Return: file path + 1-line description.
+- **Always use `context-mode__ctx_fetch_and_index`** for external docs β NEVER `eca__read_file` or `context-mode__ctx_execute` with local paths for packages you don't own
+- For GitHub-hosted projects, use the raw URL: `https://raw.githubusercontent.com/org/repo/main/CHANGELOG.md`
+- After indexing, use the `source` parameter in search to scope results to that specific document
-## Session Continuity
+## Critical Rules
-Session history is persistent and searchable. On resume, search BEFORE asking the user:
+1. **Always console.log/print your findings.** stdout is all that enters context. No output = wasted call.
+2. **Write analysis code, not just data dumps.** Don't `console.log(JSON.stringify(data))` β analyze first, print findings.
+3. **Be specific in output.** Print bug details with IDs, line numbers, exact values β not just counts.
+4. **For files you need to EDIT**: Use the normal Read tool. context-mode is for analysis, not editing.
+5. **For eca__shell_command whitelist commands only**: Use eca__shell_command for file mutations, git writes, navigation, process control, package install, and echo. Everything else goes through context-mode.
+6. **Never use `context-mode__ctx_index(content: large_data)`.** Use `context-mode__ctx_index(path: ...)` to read files server-side. The `content` parameter sends data through context as a tool parameter β use it only for small inline text.
+7. **Always use `filename` parameter** on Playwright tools (`browser_snapshot`, `browser_console_messages`, `browser_network_requests`). Without it, the full output enters context.
+8. **Don't re-index data already in context.** If an MCP tool returned data in a previous response, it's already loaded β use it directly or save to file first.
-| Need | Command |
-|------|---------|
-| What were we working on? | `context-mode__ctx_search(queries: ["summary"], sort: "timeline")` |
-| What did we decide? | `context-mode__ctx_search(queries: ["decision"], source: "decision", sort: "timeline")` |
-| What constraints exist? | `context-mode__ctx_search(queries: ["constraint"], source: "constraint")` |
+## Sandboxed Data Workflow
-DO NOT ask "what were we working on?" β SEARCH FIRST.
+
+
+ When using tools that support saving to a file: ALWAYS use the 'filename' parameter.
+ NEVER return large raw datasets directly to context.
+
+
+ LargeDataTool(filename: "path") β context-mode__ctx_index(path: "path") β context-mode__ctx_search()
+
+
-## ctx commands
+This is the universal pattern for context preservation regardless of
+the source tool (Playwright, GitHub API, AWS CLI, etc.).
-| Command | Action |
-|---------|--------|
-| `ctx stats` | Call `context-mode__ctx_stats` tool, display output verbatim |
-| `ctx doctor` | Call `context-mode__ctx_doctor` tool, run returned shell command |
-| `ctx upgrade` | Call `context-mode__ctx_upgrade` tool, run returned shell command |
-| `ctx purge` | Call `context-mode__ctx_purge` tool with confirm: true |
+## Examples
+
+### Debug an API endpoint
+```javascript
+const resp = await fetch('http://localhost:3000/api/orders');
+const { orders } = await resp.json();
+
+const bugs = [];
+const negQty = orders.filter(o => o.quantity < 0);
+if (negQty.length) bugs.push(`Negative qty: ${negQty.map(o => o.id).join(', ')}`);
+
+const nullFields = orders.filter(o => !o.product || !o.customer);
+if (nullFields.length) bugs.push(`Null fields: ${nullFields.map(o => o.id).join(', ')}`);
+
+console.log(`${orders.length} orders, ${bugs.length} bugs found:`);
+bugs.forEach(b => console.log(`- ${b}`));
+```
+
+### Analyze test output
+```shell
+npm test 2>&1
+echo "EXIT=$?"
+```
+
+### Check GitHub PRs
+```shell
+gh pr list --json number,title,state,reviewDecision --jq '.[] | "\(.number) [\(.state)] \(.title) β \(.reviewDecision // "no review")"'
+```
+
+### Read and analyze a large file
+```python
+# FILE_CONTENT is pre-loaded by context-mode__ctx_execute_file
+import json
+data = json.loads(FILE_CONTENT)
+print(f"Records: {len(data)}")
+# ... analyze and print findings
+```
+
+## Browser & Playwright Integration
+
+**When a task involves Playwright snapshots, screenshots, or page inspection, ALWAYS route through file β sandbox.**
+
+Playwright `browser_snapshot` returns 10Kβ135K tokens of accessibility tree data. Calling it without `filename` dumps all of that into context. Passing the output to `context-mode__ctx_index(content: ...)` sends it into context a SECOND time as a parameter. Both are wrong.
+
+**The key insight**: `browser_snapshot` has a `filename` parameter that saves to file instead of returning to context. `context-mode__ctx_index` has a `path` parameter that reads files server-side. `context-mode__ctx_execute_file` processes files in a sandbox. **None of these touch context.**
+
+### Workflow A: Snapshot β File β Index β Search (multiple queries)
+
+```
+Step 1: browser_snapshot(filename: "/tmp/playwright-snapshot.md")
+ β saves to file, returns ~50B confirmation (NOT 135K tokens)
+
+Step 2: context-mode__ctx_index(path: "/tmp/playwright-snapshot.md", source: "Playwright snapshot")
+ β reads file SERVER-SIDE, indexes into FTS5, returns ~80B confirmation
+
+Step 3: context-mode__ctx_search(queries: ["login form email password"], source: "Playwright")
+ β returns only matching chunks (~300B)
+```
+
+**Total context: ~430B** instead of 270K tokens. Real 99% savings.
+
+### Workflow B: Snapshot β File β Execute File (one-shot extraction)
+
+```
+Step 1: browser_snapshot(filename: "/tmp/playwright-snapshot.md")
+ β saves to file, returns ~50B confirmation
+
+Step 2: context-mode__ctx_execute_file(path: "/tmp/playwright-snapshot.md", language: "javascript", code: "
+ const links = [...FILE_CONTENT.matchAll(/- link \"([^\"]+)\"/g)].map(m => m[1]);
+ const buttons = [...FILE_CONTENT.matchAll(/- button \"([^\"]+)\"/g)].map(m => m[1]);
+ const inputs = [...FILE_CONTENT.matchAll(/- textbox|- checkbox|- radio/g)];
+ console.log('Links:', links.length, '| Buttons:', buttons.length, '| Inputs:', inputs.length);
+ console.log('Navigation:', links.slice(0, 10).join(', '));
+ ")
+ β processes in sandbox, returns ~200B summary
+```
+
+**Total context: ~250B** instead of 135K tokens.
+
+### Workflow C: Console & Network (save to file if large)
+
+```
+browser_console_messages(level: "error", filename: "/tmp/console.md")
+β context-mode__ctx_execute_file(path: "/tmp/console.md", ...) or context-mode__ctx_index(path: "/tmp/console.md", ...)
+
+browser_network_requests(includeStatic: false, filename: "/tmp/network.md")
+β context-mode__ctx_execute_file(path: "/tmp/network.md", ...) or context-mode__ctx_index(path: "/tmp/network.md", ...)
+```
+
+### CRITICAL: Why `filename` + `path` is mandatory
+
+| Approach | Context cost | Correct? |
+|----------|-------------|----------|
+| `browser_snapshot()` β raw into context | **135K tokens** | NO |
+| `browser_snapshot()` β `context-mode__ctx_index(content: raw)` | **270K tokens** (doubled!) | NO |
+| `browser_snapshot(filename)` β `context-mode__ctx_index(path)` β `context-mode__ctx_search` | **~430B** | YES |
+| `browser_snapshot(filename)` β `context-mode__ctx_execute_file(path)` | **~250B** | YES |
+
+### Key Rule
+
+> **ALWAYS use `filename` parameter when calling `browser_snapshot`, `browser_console_messages`, or `browser_network_requests`.**
+> Then process via `context-mode__ctx_index(path: ...)` or `context-mode__ctx_execute_file(path: ...)` β never `context-mode__ctx_index(content: ...)`.
+>
+> Data flow: **Playwright β file β server-side read β context**. Never: **Playwright β context β context-mode__ctx_index(content) β context again**.
+
+## Subagent Usage
+
+ECA subagents can use the same `context-mode__ctx_*` MCP tools. Keep subagent task descriptions natural, but mention context-mode explicitly when the task is likely to produce large output. Hook-based routing depends on the installed context-mode version having the ECA adapter.
+
+## Anti-Patterns
+
+- Using `curl http://api/endpoint` via eca__shell_command β 50KB floods context. Use `context-mode__ctx_execute` with fetch instead.
+- Using `cat large-file.json` via eca__shell_command β entire file in context. Use `context-mode__ctx_execute_file` instead.
+- Using `gh pr list` via eca__shell_command β raw JSON in context. Use `context-mode__ctx_execute` with `--jq` filter instead.
+- Piping eca__shell_command output through `| head -20` β you lose the rest. Use `context-mode__ctx_execute` to analyze ALL data and print summary.
+- Narrowing `context-mode__ctx_execute` output upstream of capture β `context-mode__ctx_execute` captures, `context-mode__ctx_search` filters; merging the layers drops data that the index never sees. See `references/anti-patterns.md` Β§8.
+- Running `npm test` via eca__shell_command β full test output in context. Use `context-mode__ctx_execute` to capture and summarize.
+- Calling `browser_snapshot()` WITHOUT `filename` parameter β 135K tokens flood context. **Always** use `browser_snapshot(filename: "/tmp/snap.md")`.
+- Calling `browser_console_messages()` or `browser_network_requests()` WITHOUT `filename` β entire output floods context. **Always** use the `filename` parameter.
+- Passing ANY large data to `context-mode__ctx_index(content: ...)` β data enters context as a parameter. **Always** use `context-mode__ctx_index(path: ...)` to read server-side. The `content` parameter should only be used for small inline text you're composing yourself.
+- Calling an MCP tool (Context7 `query-docs`, GitHub API, etc.) then passing the response to `context-mode__ctx_index(content: response)` β **doubles** context usage. The response is already in context β use it directly or save to file first.
+- Ignoring `browser_navigate` auto-snapshot β navigation response includes a full page snapshot. Don't rely on it for inspection β call `browser_snapshot(filename)` separately.
+- Expecting `context-mode__ctx_stats` to reset or wipe anything β `context-mode__ctx_stats` is read-only (shows stats only). Use `context-mode__ctx_purge(confirm: true)` to permanently delete all indexed content.
+
+## Reference Files
+
+- [JavaScript/TypeScript Patterns](./references/patterns-javascript.md)
+- [Python Patterns](./references/patterns-python.md)
+- [Shell Patterns](./references/patterns-shell.md)
+- [Anti-Patterns & Common Mistakes](./references/anti-patterns.md)
diff --git a/plugins/context-mode/skills/context-mode/references/anti-patterns.md b/plugins/context-mode/skills/context-mode/references/anti-patterns.md
new file mode 100644
index 0000000..156d8d2
--- /dev/null
+++ b/plugins/context-mode/skills/context-mode/references/anti-patterns.md
@@ -0,0 +1,283 @@
+# Anti-Patterns: Common Mistakes with context-mode__ctx_execute / context-mode__ctx_execute_file
+
+Avoid these pitfalls when using context-mode tools.
+
+---
+
+## 1. Using context-mode__ctx_execute for Small Outputs (< 20 Lines)
+
+**Problem:** `context-mode__ctx_execute` adds overhead (LLM summarization call). For small outputs, eca__shell_command is faster and cheaper.
+
+```
+BAD β wasteful use of context-mode__ctx_execute:
+ Tool: context-mode__ctx_execute
+ code: "echo $(node --version)"
+ language: shell
+
+GOOD β just use eca__shell_command:
+ Tool: eca__shell_command
+ command: node --version
+```
+
+**Rule:** If the output fits comfortably in your context window (under ~20 lines), use eca__shell_command directly. Reserve `context-mode__ctx_execute` for outputs that would bloat context or need intelligent summarization.
+
+More examples of "just use eca__shell_command":
+- `git status` β usually 5-10 lines
+- `ls -la` β directory listing
+- `cat .env.example` β small config file
+- `pwd`, `whoami`, `which node`
+- `wc -l src/index.ts` β single line output
+
+---
+
+## 2. Forgetting to Print Output
+
+**Problem:** `context-mode__ctx_execute` captures stdout. If your code doesn't print anything, the summary will be empty or meaningless.
+
+```javascript
+// BAD β no output:
+const fs = require('fs');
+const data = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+const deps = Object.keys(data.dependencies);
+// Nothing printed! The LLM sees empty stdout.
+
+// GOOD β explicit output:
+const fs = require('fs');
+const data = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+const deps = Object.keys(data.dependencies);
+console.log(`Dependencies (${deps.length}):`);
+deps.forEach(d => console.log(` ${d}: ${data.dependencies[d]}`));
+```
+
+```python
+# BAD β computes but never prints:
+with open('data.json') as f:
+ data = json.load(f)
+result = [x for x in data if x['status'] == 'error']
+# result is lost β never printed
+
+# GOOD β always print results:
+with open('data.json') as f:
+ data = json.load(f)
+result = [x for x in data if x['status'] == 'error']
+print(f"Found {len(result)} errors:")
+for r in result:
+ print(f" {r['id']}: {r['message']}")
+```
+
+**Rule:** Every `context-mode__ctx_execute` script must end with print/console.log of the results you want summarized.
+
+---
+
+## 3. Using shell when JS/Python Would Be Cleaner
+
+**Problem:** Complex data processing in shell quickly becomes unreadable and error-prone.
+
+```shell
+# BAD β parsing JSON in shell is fragile:
+cat data.json | python3 -c "
+import sys, json
+data = json.load(sys.stdin)
+for item in data:
+ if item['status'] == 'error':
+ print(item['id'], item['message'])
+"
+# If you're already using Python inline, just use language: python
+```
+
+```javascript
+// GOOD β use the right language for the job:
+// language: javascript
+const data = require('./data.json');
+data.filter(x => x.status === 'error')
+ .forEach(x => console.log(`${x.id}: ${x.message}`));
+```
+
+**Rule:** If your shell script contains inline Python/Node or complex `jq`/`awk` chains, switch to `language: python` or `language: javascript` instead.
+
+Signs you should switch from shell:
+- Using `python3 -c` or `node -e` inside the shell script
+- More than 3 pipes chained together
+- Using `jq` for complex JSON transformations
+- Nested loops in shell
+- String manipulation beyond simple `cut`/`sed`
+
+---
+
+## 4. Loading Entire Files into Context Then Processing
+
+**Problem:** Reading a 10,000-line file with `eca__read_file`, then asking about it, wastes your entire context window. Use `context-mode__ctx_execute` to process the file and return only the summary.
+
+```
+BAD workflow:
+ 1. eca__read_file: read 'server.log' (10,000 lines loaded into context)
+ 2. "Find all errors in this log"
+ β 10,000 lines consumed context for a question that needs ~20 lines of output
+
+GOOD workflow:
+ 1. context-mode__ctx_execute with language: python
+ code: |
+ with open('server.log') as f:
+ errors = [l for l in f if 'ERROR' in l]
+ print(f"Total errors: {len(errors)}")
+ for e in errors[-20:]:
+ print(e.strip())
+ summary_prompt: "Categorize errors and report frequency"
+ β Only the summary enters context
+```
+
+```
+BAD workflow:
+ 1. eca__read_file: read 'package-lock.json' (20,000 lines)
+ 2. "What version of lodash is installed?"
+
+GOOD workflow:
+ 1. context-mode__ctx_execute with language: javascript
+ code: |
+ const lock = require('./package-lock.json');
+ const find = (deps, name) => {
+ if (deps[name]) return deps[name].version;
+ for (const [, dep] of Object.entries(deps)) {
+ if (dep.dependencies) {
+ const v = find(dep.dependencies, name);
+ if (v) return v;
+ }
+ }
+ };
+ console.log(`lodash: ${find(lock.dependencies, 'lodash') || 'not found'}`);
+ summary_prompt: "Report the installed version of lodash"
+```
+
+**Rule:** If a file is over 200 lines and you only need specific data from it, use `context-mode__ctx_execute` to extract what you need rather than reading the whole file into context.
+
+---
+
+## 5. Not Using JSON.stringify for Structured Output
+
+**Problem:** Printing objects without serialization gives `[object Object]` in JavaScript.
+
+```javascript
+// BAD β prints [object Object]:
+const pkg = require('./package.json');
+console.log(pkg.dependencies);
+// Output: [object Object]
+
+// GOOD β serialize properly:
+const pkg = require('./package.json');
+console.log(JSON.stringify(pkg.dependencies, null, 2));
+// Output: { "react": "^18.2.0", "next": "^14.0.0", ... }
+```
+
+```javascript
+// BAD β loses structure in arrays:
+const items = [{name: 'a', value: 1}, {name: 'b', value: 2}];
+console.log(items);
+// May print unhelpfully
+
+// GOOD β format as table:
+const items = [{name: 'a', value: 1}, {name: 'b', value: 2}];
+console.log('Name | Value');
+console.log('------|------');
+items.forEach(i => console.log(`${i.name.padEnd(5)} | ${i.value}`));
+// Or use JSON.stringify:
+console.log(JSON.stringify(items, null, 2));
+```
+
+**Rule:** Always use `JSON.stringify(data, null, 2)` for objects/arrays in JavaScript, or format as a readable table. In Python, use `json.dumps(data, indent=2)` or `pprint.pprint(data)`.
+
+---
+
+## 6. Timeout Too Short for Network Operations
+
+**Problem:** Default timeout may be too short for API calls, builds, or test suites.
+
+```
+BAD β will timeout on API calls:
+ Tool: context-mode__ctx_execute
+ code: |
+ const resp = await fetch('https://api.slow-service.com/data');
+ console.log(await resp.json());
+ language: javascript
+ timeout_ms: 5000 β API may take 10+ seconds
+
+GOOD β generous timeout for network:
+ Tool: context-mode__ctx_execute
+ code: |
+ const resp = await fetch('https://api.slow-service.com/data');
+ console.log(JSON.stringify(await resp.json(), null, 2));
+ language: javascript
+ timeout_ms: 30000 β 30 seconds for network calls
+```
+
+**Recommended timeouts:**
+| Operation | timeout_ms |
+|-----------|-----------|
+| File reading/parsing | 5000 - 10000 |
+| Local computation | 10000 |
+| Single API request | 15000 - 30000 |
+| Paginated API calls | 30000 - 60000 |
+| npm install / build | 120000 |
+| Full test suite | 120000 - 300000 |
+
+**Rule:** Always consider what your script does and set `timeout_ms` accordingly. Network calls and builds need significantly more time than file operations.
+
+---
+
+## 7. Not Using summary_prompt Effectively
+
+**Problem:** Without a good `summary_prompt`, the LLM summarization may focus on irrelevant details.
+
+```
+BAD β vague or missing summary_prompt:
+ summary_prompt: "Summarize this"
+ β May focus on the wrong aspects
+
+GOOD β specific and actionable:
+ summary_prompt: "Report the count of failing tests, list each failure with its file path and error message, and identify any patterns in the failures"
+```
+
+**Tips for effective summary_prompt:**
+- Be specific about what data points you need
+- Ask for counts and metrics, not just descriptions
+- Request actionable insights ("suggest fixes", "identify patterns")
+- Mention the format you want ("list as bullet points", "group by category")
+
+---
+
+## 8. `context-mode__ctx_execute` Captures, `context-mode__ctx_search` Filters β Don't Merge the Layers
+
+`context-mode__ctx_execute` and `context-mode__ctx_search` are two layers, not one. `context-mode__ctx_execute` exists to **capture** full output into the index. `context-mode__ctx_search` exists to **filter** what was captured. When you narrow the output *inside* `context-mode__ctx_execute` β at the shell layer, in script logic, anywhere upstream of capture β the dropped lines never reach the index. `context-mode__ctx_search` cannot recover what was never written. You've spent the capture budget and lost the data you'd want to query later, for no context-window benefit: large stdout is already auto-indexed, not returned inline.
+
+The mental model:
+
+```
+ββββββββββββββββββββββββ ββββββββββββββββββββββββ
+β context-mode__ctx_execute β ββββΆ β context-mode__ctx_search β
+β (capture layer) β β (filter layer) β
+β β β β
+β produces full β β queries the β
+β output into index β β captured index β
+ββββββββββββββββββββββββ ββββββββββββββββββββββββ
+ β² β²
+ β β
+ Job: capture Job: narrow
+ Do NOT narrow here. Do all narrowing here.
+```
+
+**Rule:** Treat `context-mode__ctx_execute`'s output as write-once to the index. Run the command in full and let it index. Do every narrowing step downstream, via `context-mode__ctx_search`. If you find yourself trimming inside `context-mode__ctx_execute`, you are doing the filter layer's job in the capture layer β stop and move the narrowing to a `context-mode__ctx_search` call.
+
+**Why the layer separation matters:** the index is what survives across calls and across sessions. Anything you discard before the index is gone permanently from this session's queryable surface. Anything you keep is queryable, repeatedly, with different questions, at zero re-execution cost.
+
+---
+
+## Summary Checklist
+
+Before using `context-mode__ctx_execute`, verify:
+
+- [ ] Output will be > 20 lines (otherwise use eca__shell_command)
+- [ ] Script prints all results to stdout
+- [ ] Objects are serialized with JSON.stringify / json.dumps
+- [ ] Timeout matches the operation type
+- [ ] Language matches the task (JS for JSON/API, Python for data, Shell for pipes)
+- [ ] summary_prompt is specific and actionable
+- [ ] Not loading a file into context that could be processed inside context-mode__ctx_execute
diff --git a/plugins/context-mode/skills/context-mode/references/patterns-javascript.md b/plugins/context-mode/skills/context-mode/references/patterns-javascript.md
new file mode 100644
index 0000000..c52c714
--- /dev/null
+++ b/plugins/context-mode/skills/context-mode/references/patterns-javascript.md
@@ -0,0 +1,298 @@
+# JavaScript / TypeScript Patterns for context-mode__ctx_execute
+
+Practical patterns for using `context-mode__ctx_execute` with `language: javascript`.
+All examples assume Node.js runtime with native fetch (Node 18+).
+
+---
+
+## API Response Processing
+
+### Fetch and summarize a REST API
+
+```javascript
+// context-mode__ctx_execute: Analyze API health endpoint
+const resp = await fetch('https://api.example.com/health');
+const data = await resp.json();
+
+console.log('=== Service Health ===');
+console.log(`Status: ${data.status}`);
+console.log(`Uptime: ${data.uptime}`);
+console.log(`Timestamp: ${data.timestamp}`);
+
+if (data.services) {
+ console.log('\n=== Service Components ===');
+ for (const [name, info] of Object.entries(data.services)) {
+ console.log(` ${name}: ${info.status} (latency: ${info.latency_ms}ms)`);
+ }
+}
+
+if (data.errors && data.errors.length > 0) {
+ console.log('\n=== Recent Errors ===');
+ data.errors.slice(0, 10).forEach(e => {
+ console.log(` [${e.timestamp}] ${e.code}: ${e.message}`);
+ });
+}
+```
+> summary_prompt: "Report overall health, list any degraded services, and highlight errors"
+
+### Paginated API collection
+
+```javascript
+// context-mode__ctx_execute: Fetch all open issues from GitHub API
+const owner = 'org';
+const repo = 'project';
+let page = 1;
+let allIssues = [];
+
+while (true) {
+ const resp = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}/issues?state=open&per_page=100&page=${page}`,
+ { headers: { 'Accept': 'application/vnd.github.v3+json' } }
+ );
+ const issues = await resp.json();
+ if (issues.length === 0) break;
+ allIssues.push(...issues);
+ page++;
+}
+
+console.log(`Total open issues: ${allIssues.length}\n`);
+
+// Group by labels
+const byLabel = {};
+allIssues.forEach(issue => {
+ issue.labels.forEach(label => {
+ byLabel[label.name] = (byLabel[label.name] || 0) + 1;
+ });
+});
+
+console.log('=== Issues by Label ===');
+Object.entries(byLabel)
+ .sort((a, b) => b[1] - a[1])
+ .forEach(([label, count]) => console.log(` ${label}: ${count}`));
+
+// Oldest issues
+console.log('\n=== 10 Oldest Issues ===');
+allIssues
+ .sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
+ .slice(0, 10)
+ .forEach(i => console.log(` #${i.number} (${i.created_at.slice(0,10)}): ${i.title}`));
+```
+> summary_prompt: "Summarize issue distribution by label, highlight stale issues, suggest priorities"
+> timeout_ms: 30000
+
+---
+
+## JSON Data Analysis
+
+### Analyze a large JSON config file
+
+```javascript
+const fs = require('fs');
+const data = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8'));
+
+console.log('=== TSConfig Analysis ===');
+console.log(`Target: ${data.compilerOptions?.target}`);
+console.log(`Module: ${data.compilerOptions?.module}`);
+console.log(`Strict: ${data.compilerOptions?.strict}`);
+console.log(`Paths aliases: ${Object.keys(data.compilerOptions?.paths || {}).length}`);
+
+if (data.compilerOptions?.paths) {
+ console.log('\n=== Path Aliases ===');
+ for (const [alias, targets] of Object.entries(data.compilerOptions.paths)) {
+ console.log(` ${alias} -> ${targets.join(', ')}`);
+ }
+}
+
+if (data.include) console.log(`\nInclude: ${data.include.join(', ')}`);
+if (data.exclude) console.log(`Exclude: ${data.exclude.join(', ')}`);
+if (data.references) {
+ console.log(`\nProject References: ${data.references.length}`);
+ data.references.forEach(r => console.log(` ${r.path}`));
+}
+```
+> summary_prompt: "Report compiler strictness, module system, and any unusual configuration"
+
+### Diff two JSON files
+
+```javascript
+const fs = require('fs');
+const a = JSON.parse(fs.readFileSync('config.prod.json', 'utf8'));
+const b = JSON.parse(fs.readFileSync('config.staging.json', 'utf8'));
+
+function diffObjects(obj1, obj2, path = '') {
+ const allKeys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]);
+ for (const key of allKeys) {
+ const fullPath = path ? `${path}.${key}` : key;
+ if (!(key in (obj1 || {}))) {
+ console.log(`+ ${fullPath}: ${JSON.stringify(obj2[key])}`);
+ } else if (!(key in (obj2 || {}))) {
+ console.log(`- ${fullPath}: ${JSON.stringify(obj1[key])}`);
+ } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
+ diffObjects(obj1[key], obj2[key], fullPath);
+ } else if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
+ console.log(`~ ${fullPath}: ${JSON.stringify(obj1[key])} -> ${JSON.stringify(obj2[key])}`);
+ }
+ }
+}
+
+console.log('=== Config Diff: prod vs staging ===');
+diffObjects(a, b);
+```
+> summary_prompt: "List all configuration differences between prod and staging environments"
+
+---
+
+## Package.json / Lock File Analysis
+
+### Dependency audit
+
+```javascript
+const fs = require('fs');
+const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+
+const deps = Object.entries(pkg.dependencies || {});
+const devDeps = Object.entries(pkg.devDependencies || {});
+
+console.log(`Package: ${pkg.name}@${pkg.version}`);
+console.log(`Dependencies: ${deps.length}`);
+console.log(`DevDependencies: ${devDeps.length}`);
+
+// Find non-pinned versions
+console.log('\n=== Non-Pinned Dependencies ===');
+[...deps, ...devDeps].forEach(([name, version]) => {
+ if (version.startsWith('^') || version.startsWith('~') || version === '*') {
+ console.log(` ${name}: ${version}`);
+ }
+});
+
+// Find duplicated categories
+console.log('\n=== Scripts ===');
+Object.entries(pkg.scripts || {}).forEach(([name, cmd]) => {
+ console.log(` ${name}: ${cmd}`);
+});
+
+// Workspace detection
+if (pkg.workspaces) {
+ console.log('\n=== Monorepo Workspaces ===');
+ const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
+ ws.forEach(w => console.log(` ${w}`));
+}
+```
+> summary_prompt: "Report dependency health: unpinned versions, total count, any security concerns from package names"
+
+### Lock file drift detection
+
+```javascript
+const fs = require('fs');
+const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+
+let lockExists = { npm: false, yarn: false, pnpm: false };
+try { fs.accessSync('package-lock.json'); lockExists.npm = true; } catch {}
+try { fs.accessSync('yarn.lock'); lockExists.yarn = true; } catch {}
+try { fs.accessSync('pnpm-lock.yaml'); lockExists.pnpm = true; } catch {}
+
+console.log('=== Lock File Status ===');
+Object.entries(lockExists).forEach(([mgr, exists]) => {
+ console.log(` ${mgr}: ${exists ? 'PRESENT' : 'missing'}`);
+});
+
+const activeLocks = Object.entries(lockExists).filter(([, v]) => v);
+if (activeLocks.length > 1) {
+ console.log('\nWARNING: Multiple lock files detected! This causes inconsistent installs.');
+}
+if (activeLocks.length === 0) {
+ console.log('\nWARNING: No lock file found! Dependencies are not reproducible.');
+}
+
+// Check engines
+if (pkg.engines) {
+ console.log('\n=== Required Engines ===');
+ Object.entries(pkg.engines).forEach(([e, v]) => console.log(` ${e}: ${v}`));
+}
+```
+> summary_prompt: "Report lock file health and any warnings about package management"
+
+---
+
+## File Content Parsing
+
+### Parse and summarize a large markdown file
+
+```javascript
+const fs = require('fs');
+const content = fs.readFileSync('CHANGELOG.md', 'utf8');
+const lines = content.split('\n');
+
+const sections = [];
+let currentSection = null;
+
+for (const line of lines) {
+ if (line.startsWith('## ')) {
+ if (currentSection) sections.push(currentSection);
+ currentSection = { title: line.replace('## ', ''), items: 0, breaking: 0 };
+ } else if (currentSection && line.startsWith('- ')) {
+ currentSection.items++;
+ if (line.toLowerCase().includes('breaking') || line.toLowerCase().includes('BREAKING')) {
+ currentSection.breaking++;
+ }
+ }
+}
+if (currentSection) sections.push(currentSection);
+
+console.log(`Total versions: ${sections.length}\n`);
+console.log('=== Recent Versions ===');
+sections.slice(0, 10).forEach(s => {
+ const warn = s.breaking > 0 ? ` [${s.breaking} BREAKING]` : '';
+ console.log(` ${s.title}: ${s.items} changes${warn}`);
+});
+
+const totalBreaking = sections.reduce((sum, s) => sum + s.breaking, 0);
+if (totalBreaking > 0) {
+ console.log(`\nTotal breaking changes across all versions: ${totalBreaking}`);
+}
+```
+> summary_prompt: "Summarize recent releases, highlight breaking changes, report release cadence"
+
+---
+
+## Test Output Parsing
+
+### Run tests and extract failures
+
+```javascript
+const { execSync } = require('child_process');
+
+let output;
+try {
+ output = execSync('npx jest --json 2>/dev/null', { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
+} catch (e) {
+ output = e.stdout || '';
+}
+
+try {
+ const results = JSON.parse(output);
+ console.log(`=== Test Results ===`);
+ console.log(`Suites: ${results.numPassedTestSuites} passed, ${results.numFailedTestSuites} failed`);
+ console.log(`Tests: ${results.numPassedTests} passed, ${results.numFailedTests} failed`);
+ console.log(`Time: ${(results.testResults || []).reduce((s, t) => s + (t.endTime - t.startTime), 0)}ms`);
+
+ const failures = (results.testResults || []).filter(t => t.status === 'failed');
+ if (failures.length > 0) {
+ console.log('\n=== Failed Tests ===');
+ failures.forEach(suite => {
+ console.log(`\nSuite: ${suite.name}`);
+ (suite.assertionResults || [])
+ .filter(a => a.status === 'failed')
+ .forEach(a => {
+ console.log(` FAIL: ${a.ancestorTitles.join(' > ')} > ${a.title}`);
+ console.log(` ${(a.failureMessages || []).join('\n ').slice(0, 200)}`);
+ });
+ });
+ }
+} catch {
+ console.log('Could not parse JSON output. Raw output:');
+ console.log(output.slice(0, 5000));
+}
+```
+> summary_prompt: "Report test pass/fail counts, list each failing test with its error message"
+> timeout_ms: 60000
diff --git a/plugins/context-mode/skills/context-mode/references/patterns-python.md b/plugins/context-mode/skills/context-mode/references/patterns-python.md
new file mode 100644
index 0000000..794164f
--- /dev/null
+++ b/plugins/context-mode/skills/context-mode/references/patterns-python.md
@@ -0,0 +1,304 @@
+# Python Patterns for context-mode__ctx_execute
+
+Practical patterns for using `context-mode__ctx_execute` with `language: python`.
+All examples use Python standard library only (no pip installs required).
+
+---
+
+## Data Processing with json Module
+
+### Analyze a large JSON dataset
+
+```python
+import json
+
+with open('data/users.json') as f:
+ users = json.load(f)
+
+print(f"Total users: {len(users)}")
+
+# Group by status
+from collections import Counter
+statuses = Counter(u.get('status', 'unknown') for u in users)
+print("\n=== Users by Status ===")
+for status, count in statuses.most_common():
+ print(f" {status}: {count}")
+
+# Find anomalies
+inactive_with_recent = [
+ u for u in users
+ if u.get('status') == 'inactive' and u.get('last_login', '') > '2025-01-01'
+]
+if inactive_with_recent:
+ print(f"\n=== Anomaly: {len(inactive_with_recent)} inactive users with recent logins ===")
+ for u in inactive_with_recent[:10]:
+ print(f" {u['email']} - last login: {u['last_login']}")
+
+# Field completeness
+fields = ['name', 'email', 'phone', 'address']
+print("\n=== Field Completeness ===")
+for field in fields:
+ filled = sum(1 for u in users if u.get(field))
+ pct = (filled / len(users)) * 100 if users else 0
+ print(f" {field}: {filled}/{len(users)} ({pct:.1f}%)")
+```
+> summary_prompt: "Report user distribution, data quality issues, and any anomalies found"
+
+### Merge and compare two JSON configs
+
+```python
+import json
+
+with open('config.default.json') as f:
+ defaults = json.load(f)
+with open('config.local.json') as f:
+ local = json.load(f)
+
+def compare(d1, d2, path=""):
+ diffs = []
+ all_keys = set(list(d1.keys()) + list(d2.keys()))
+ for key in sorted(all_keys):
+ full_path = f"{path}.{key}" if path else key
+ if key not in d1:
+ diffs.append(f" + {full_path} = {json.dumps(d2[key])}")
+ elif key not in d2:
+ diffs.append(f" - {full_path} = {json.dumps(d1[key])}")
+ elif isinstance(d1[key], dict) and isinstance(d2[key], dict):
+ diffs.extend(compare(d1[key], d2[key], full_path))
+ elif d1[key] != d2[key]:
+ diffs.append(f" ~ {full_path}: {json.dumps(d1[key])} -> {json.dumps(d2[key])}")
+ return diffs
+
+diffs = compare(defaults, local)
+print(f"Config differences: {len(diffs)}")
+if diffs:
+ print("\n=== Changes (local overrides) ===")
+ for d in diffs:
+ print(d)
+else:
+ print("No differences found β local matches defaults.")
+```
+> summary_prompt: "List all local config overrides and flag any potentially dangerous changes"
+
+---
+
+## CSV / Log File Analysis
+
+### Analyze a CSV file
+
+```python
+import csv
+from collections import Counter, defaultdict
+from datetime import datetime
+
+with open('data/transactions.csv') as f:
+ reader = csv.DictReader(f)
+ rows = list(reader)
+
+print(f"Total records: {len(rows)}")
+print(f"Columns: {', '.join(rows[0].keys()) if rows else 'none'}")
+
+# Summary statistics for numeric column
+amounts = [float(r['amount']) for r in rows if r.get('amount')]
+if amounts:
+ print(f"\n=== Amount Statistics ===")
+ print(f" Min: ${min(amounts):,.2f}")
+ print(f" Max: ${max(amounts):,.2f}")
+ print(f" Mean: ${sum(amounts)/len(amounts):,.2f}")
+ print(f" Median: ${sorted(amounts)[len(amounts)//2]:,.2f}")
+ print(f" Total: ${sum(amounts):,.2f}")
+
+# Group by category
+if 'category' in rows[0]:
+ by_cat = defaultdict(list)
+ for r in rows:
+ by_cat[r['category']].append(float(r.get('amount', 0)))
+ print("\n=== By Category ===")
+ for cat, vals in sorted(by_cat.items(), key=lambda x: -sum(x[1])):
+ print(f" {cat}: {len(vals)} txns, total ${sum(vals):,.2f}")
+```
+> summary_prompt: "Summarize transaction patterns, highlight outliers, report category distribution"
+
+### Parse application logs
+
+```python
+import re
+from collections import Counter
+from datetime import datetime
+
+error_pattern = re.compile(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.+)')
+
+levels = Counter()
+errors_by_type = Counter()
+hourly = Counter()
+
+with open('app.log') as f:
+ for line in f:
+ match = error_pattern.match(line.strip())
+ if match:
+ timestamp, level, message = match.groups()
+ levels[level] += 1
+ hour = timestamp[:13]
+ hourly[hour] += 1
+ if level in ('ERROR', 'FATAL'):
+ # Extract error class
+ err_type = message.split(':')[0].strip()
+ errors_by_type[err_type] += 1
+
+print("=== Log Level Distribution ===")
+for level, count in levels.most_common():
+ print(f" {level}: {count}")
+
+print("\n=== Top Error Types ===")
+for err, count in errors_by_type.most_common(10):
+ print(f" {err}: {count}")
+
+print("\n=== Hourly Activity (last 24h) ===")
+for hour, count in sorted(hourly.items())[-24:]:
+ bar = '#' * min(count // 10, 50)
+ print(f" {hour}: {count:>5} {bar}")
+```
+> summary_prompt: "Report error rates, identify the most common failures, and note any traffic spikes"
+
+---
+
+## Text Extraction and Summarization
+
+### Extract TODOs and FIXMEs from codebase
+
+```python
+import os
+import re
+
+pattern = re.compile(r'(TODO|FIXME|HACK|XXX|WARN)[:\s](.+)', re.IGNORECASE)
+results = []
+
+for root, dirs, files in os.walk('src'):
+ # Skip node_modules and hidden dirs
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules']
+ for fname in files:
+ if fname.endswith(('.ts', '.tsx', '.js', '.jsx', '.py')):
+ filepath = os.path.join(root, fname)
+ with open(filepath) as f:
+ for i, line in enumerate(f, 1):
+ match = pattern.search(line)
+ if match:
+ results.append({
+ 'file': filepath,
+ 'line': i,
+ 'type': match.group(1).upper(),
+ 'text': match.group(2).strip()
+ })
+
+from collections import Counter
+by_type = Counter(r['type'] for r in results)
+
+print(f"Total annotations found: {len(results)}\n")
+print("=== By Type ===")
+for t, c in by_type.most_common():
+ print(f" {t}: {c}")
+
+print("\n=== All Items ===")
+for r in results:
+ print(f" [{r['type']}] {r['file']}:{r['line']} β {r['text'][:100]}")
+```
+> summary_prompt: "Categorize TODOs by urgency, group by file area, suggest which to address first"
+
+### Summarize a large text/markdown file
+
+```python
+with open('ARCHITECTURE.md') as f:
+ content = f.read()
+
+lines = content.split('\n')
+print(f"Total lines: {len(lines)}")
+print(f"Total words: {len(content.split())}")
+
+# Extract structure
+headings = [(i+1, line) for i, line in enumerate(lines) if line.startswith('#')]
+print(f"Sections: {len(headings)}\n")
+
+print("=== Document Structure ===")
+for line_num, heading in headings:
+ level = len(heading) - len(heading.lstrip('#'))
+ indent = ' ' * (level - 1)
+ print(f" {indent}{heading.strip()} (line {line_num})")
+
+# Extract code blocks
+import re
+code_blocks = re.findall(r'```(\w+)?', content)
+if code_blocks:
+ from collections import Counter
+ langs = Counter(b for b in code_blocks if b)
+ print(f"\n=== Code Blocks: {len(code_blocks)} total ===")
+ for lang, count in langs.most_common():
+ print(f" {lang}: {count}")
+
+# Print first 50 lines for content preview
+print("\n=== Content Preview (first 50 lines) ===")
+for line in lines[:50]:
+ print(line)
+```
+> summary_prompt: "Summarize the document structure, key architectural decisions, and main components described"
+
+---
+
+## File Comparison
+
+### Compare two source files
+
+```python
+import difflib
+
+with open('src/auth/login.ts') as f:
+ old_lines = f.readlines()
+with open('src/auth/login.new.ts') as f:
+ new_lines = f.readlines()
+
+diff = list(difflib.unified_diff(old_lines, new_lines, fromfile='login.ts', tofile='login.new.ts', lineterm=''))
+
+additions = sum(1 for l in diff if l.startswith('+') and not l.startswith('+++'))
+deletions = sum(1 for l in diff if l.startswith('-') and not l.startswith('---'))
+
+print(f"Changes: +{additions} -{deletions}\n")
+
+if diff:
+ print("=== Diff ===")
+ for line in diff:
+ print(line)
+else:
+ print("Files are identical.")
+```
+> summary_prompt: "Describe the functional changes between the old and new versions"
+
+### Find duplicate content across files
+
+```python
+import os
+import hashlib
+from collections import defaultdict
+
+file_hashes = defaultdict(list)
+
+for root, dirs, files in os.walk('src'):
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules']
+ for fname in files:
+ if fname.endswith(('.ts', '.tsx', '.js', '.jsx')):
+ filepath = os.path.join(root, fname)
+ with open(filepath, 'rb') as f:
+ content_hash = hashlib.md5(f.read()).hexdigest()
+ file_hashes[content_hash].append(filepath)
+
+duplicates = {h: files for h, files in file_hashes.items() if len(files) > 1}
+
+if duplicates:
+ print(f"Found {len(duplicates)} sets of duplicate files:\n")
+ for h, files in duplicates.items():
+ print(f" Hash: {h[:8]}...")
+ for f in files:
+ print(f" {f}")
+ print()
+else:
+ print("No duplicate files found.")
+```
+> summary_prompt: "List all duplicate files and suggest which copies to remove"
diff --git a/plugins/context-mode/skills/context-mode/references/patterns-shell.md b/plugins/context-mode/skills/context-mode/references/patterns-shell.md
new file mode 100644
index 0000000..9e0501e
--- /dev/null
+++ b/plugins/context-mode/skills/context-mode/references/patterns-shell.md
@@ -0,0 +1,277 @@
+# Shell Patterns for context-mode__ctx_execute
+
+Practical patterns for using `context-mode__ctx_execute` with `language: shell`.
+Best for piping, filtering, and leveraging native OS tools.
+
+---
+
+## Build Output Filtering
+
+### Capture build errors only
+
+```shell
+npm run build 2>&1 | tee /tmp/build-output.txt
+EXIT_CODE=${PIPESTATUS[0]}
+
+echo "=== Build Result ==="
+echo "Exit code: $EXIT_CODE"
+
+if [ "$EXIT_CODE" -ne 0 ]; then
+ echo ""
+ echo "=== Errors ==="
+ grep -iE '(error|failed|FAIL)' /tmp/build-output.txt | head -50
+ echo ""
+ echo "=== Warnings ==="
+ grep -iE '(warning|warn)' /tmp/build-output.txt | head -20
+else
+ echo "Build succeeded."
+ echo ""
+ echo "=== Warnings (if any) ==="
+ grep -iE '(warning|warn)' /tmp/build-output.txt | head -10
+fi
+
+echo ""
+echo "=== Output Size ==="
+wc -l < /tmp/build-output.txt | xargs -I{} echo "{} total lines of output"
+rm -f /tmp/build-output.txt
+```
+> summary_prompt: "Report build success/failure, list all errors with file paths, and count warnings"
+> timeout_ms: 120000
+
+### TypeScript compilation check
+
+```shell
+npx tsc --noEmit 2>&1 | tee /tmp/tsc-output.txt
+EXIT_CODE=${PIPESTATUS[0]}
+
+echo "=== TypeScript Check ==="
+echo "Exit code: $EXIT_CODE"
+
+TOTAL_ERRORS=$(grep -c 'error TS' /tmp/tsc-output.txt 2>/dev/null || echo 0)
+echo "Total errors: $TOTAL_ERRORS"
+
+if [ "$TOTAL_ERRORS" -gt 0 ]; then
+ echo ""
+ echo "=== Errors by Code ==="
+ grep -oP 'error TS\d+' /tmp/tsc-output.txt | sort | uniq -c | sort -rn | head -20
+
+ echo ""
+ echo "=== Errors by File ==="
+ grep 'error TS' /tmp/tsc-output.txt | cut -d'(' -f1 | sort | uniq -c | sort -rn | head -20
+
+ echo ""
+ echo "=== First 30 Errors ==="
+ grep 'error TS' /tmp/tsc-output.txt | head -30
+fi
+
+rm -f /tmp/tsc-output.txt
+```
+> summary_prompt: "Report type error count, most common error codes, and most affected files"
+> timeout_ms: 60000
+
+---
+
+## Test Result Summarization
+
+### Jest test summary
+
+```shell
+npx jest --verbose 2>&1 | tee /tmp/test-output.txt
+EXIT_CODE=${PIPESTATUS[0]}
+
+echo ""
+echo "=== Test Summary ==="
+echo "Exit code: $EXIT_CODE"
+
+# Extract summary line
+grep -E '(Tests:|Test Suites:|Snapshots:|Time:)' /tmp/test-output.txt
+
+echo ""
+echo "=== Failed Tests ==="
+grep -A 2 'FAIL ' /tmp/test-output.txt | head -40
+
+echo ""
+echo "=== Slow Tests (if reported) ==="
+grep -i 'slow' /tmp/test-output.txt | head -10
+
+rm -f /tmp/test-output.txt
+```
+> summary_prompt: "Report pass/fail ratio, list all failing test names with suite, note any slow tests"
+> timeout_ms: 120000
+
+### Pytest summary
+
+```shell
+python -m pytest --tb=short -q 2>&1 | tee /tmp/pytest-output.txt
+EXIT_CODE=${PIPESTATUS[0]}
+
+echo ""
+echo "=== Pytest Summary ==="
+echo "Exit code: $EXIT_CODE"
+
+# Last 20 lines usually contain the summary
+tail -20 /tmp/pytest-output.txt
+
+echo ""
+echo "=== Failures ==="
+grep -E '(FAILED|ERROR)' /tmp/pytest-output.txt | head -30
+
+rm -f /tmp/pytest-output.txt
+```
+> summary_prompt: "Report test results, list all failures with file and test name"
+> timeout_ms: 120000
+
+---
+
+## Log File Analysis
+
+### Filter application logs by severity
+
+```shell
+LOG_FILE="${1:-/var/log/app.log}"
+
+echo "=== Log File: $LOG_FILE ==="
+echo "Total lines: $(wc -l < "$LOG_FILE")"
+echo ""
+
+echo "=== Level Distribution ==="
+grep -oE '\b(DEBUG|INFO|WARN|ERROR|FATAL)\b' "$LOG_FILE" | sort | uniq -c | sort -rn
+
+echo ""
+echo "=== Last 20 Errors ==="
+grep -i 'ERROR\|FATAL' "$LOG_FILE" | tail -20
+
+echo ""
+echo "=== Error Timeline (hourly) ==="
+grep -i 'ERROR' "$LOG_FILE" | grep -oE '\d{4}-\d{2}-\d{2} \d{2}' | sort | uniq -c | tail -24
+```
+> summary_prompt: "Report error frequency, identify patterns, and note any error spikes"
+
+### Analyze access logs
+
+```shell
+LOG_FILE="${1:-/var/log/access.log}"
+
+echo "=== Access Log Summary ==="
+echo "Total requests: $(wc -l < "$LOG_FILE")"
+
+echo ""
+echo "=== HTTP Status Codes ==="
+awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
+
+echo ""
+echo "=== Top 20 Paths ==="
+awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -20
+
+echo ""
+echo "=== Top 10 IPs ==="
+awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
+
+echo ""
+echo "=== 5xx Errors ==="
+awk '$9 ~ /^5/' "$LOG_FILE" | tail -20
+
+echo ""
+echo "=== Requests per Hour ==="
+awk '{print $4}' "$LOG_FILE" | cut -d: -f1-2 | sort | uniq -c | tail -24
+```
+> summary_prompt: "Report traffic patterns, error rates, most hit endpoints, and suspicious IPs"
+
+---
+
+## Directory Size and Structure Analysis
+
+### Project structure overview
+
+```shell
+echo "=== Directory Structure ==="
+find . -maxdepth 3 -type d \
+ ! -path '*/node_modules/*' \
+ ! -path '*/.git/*' \
+ ! -path '*/dist/*' \
+ ! -path '*/.next/*' \
+ ! -path '*/__pycache__/*' \
+ | sort
+
+echo ""
+echo "=== File Type Distribution ==="
+find . -type f \
+ ! -path '*/node_modules/*' \
+ ! -path '*/.git/*' \
+ ! -path '*/dist/*' \
+ | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -20
+
+echo ""
+echo "=== Largest Files (top 20) ==="
+find . -type f \
+ ! -path '*/node_modules/*' \
+ ! -path '*/.git/*' \
+ -exec ls -la {} \; | sort -k5 -rn | head -20 | awk '{print $5, $9}'
+
+echo ""
+echo "=== Directory Sizes ==="
+du -sh */ 2>/dev/null | sort -rh | head -15
+```
+> summary_prompt: "Describe the project structure, identify large files that may need attention, report file type distribution"
+
+### Disk usage investigation
+
+```shell
+echo "=== Top-Level Disk Usage ==="
+du -sh */ 2>/dev/null | sort -rh
+
+echo ""
+echo "=== node_modules Size ==="
+if [ -d "node_modules" ]; then
+ du -sh node_modules
+ echo ""
+ echo "=== Largest node_modules packages ==="
+ du -sh node_modules/*/ 2>/dev/null | sort -rh | head -20
+else
+ echo "No node_modules directory"
+fi
+
+echo ""
+echo "=== Build Artifacts ==="
+for dir in dist build .next out .cache; do
+ if [ -d "$dir" ]; then
+ echo " $dir: $(du -sh "$dir" | cut -f1)"
+ fi
+done
+
+echo ""
+echo "=== Git Objects Size ==="
+if [ -d ".git" ]; then
+ du -sh .git
+fi
+```
+> summary_prompt: "Report total project size, largest contributors, and recommend cleanup targets"
+
+---
+
+## Git Analysis
+
+### Commit activity analysis
+
+```shell
+echo "=== Recent Commits (last 30 days) ==="
+git log --since="30 days ago" --oneline | wc -l | xargs -I{} echo "{} commits in last 30 days"
+
+echo ""
+echo "=== Commits by Author ==="
+git shortlog -sn --since="30 days ago" | head -15
+
+echo ""
+echo "=== Most Changed Files (last 30 days) ==="
+git log --since="30 days ago" --pretty=format: --name-only | sort | uniq -c | sort -rn | head -20
+
+echo ""
+echo "=== Branches ==="
+echo "Local: $(git branch | wc -l | xargs)"
+echo "Remote: $(git branch -r | wc -l | xargs)"
+
+echo ""
+echo "=== Stale Branches (merged, excluding main/master) ==="
+git branch --merged main 2>/dev/null | grep -v 'main\|master\|\*' | head -10
+```
+> summary_prompt: "Report development velocity, active contributors, hotspot files, and cleanup opportunities"
diff --git a/plugins/context-mode/skills/ctx-doctor/SKILL.md b/plugins/context-mode/skills/ctx-doctor/SKILL.md
new file mode 100644
index 0000000..a501440
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-doctor/SKILL.md
@@ -0,0 +1,21 @@
+---
+name: ctx-doctor
+description: |
+ Run context-mode diagnostics. Checks runtimes, hooks, FTS5,
+ plugin registration, npm and marketplace versions.
+ Trigger: /context-mode:ctx-doctor
+---
+
+# Context Mode Doctor
+
+Run diagnostics and display results directly in the conversation.
+
+## Instructions
+
+1. Call the `context-mode__ctx_doctor` MCP tool directly. It runs all checks server-side and returns a plain-text status report.
+2. Display the results verbatim β they are already formatted with plain-text status prefixes: `[OK]` PASS, `[FAIL]` FAIL, `[WARN]` WARN. Renderer-safe (no markdown task-list syntax) for cross-client compatibility (e.g., Z.ai GLM).
+3. **Fallback** (only if MCP tool call fails): run the installed CLI with `eca__shell_command`:
+ ```bash
+ context-mode doctor
+ ```
+ Re-display results verbatim with the same `[OK]`/`[FAIL]`/`[WARN]` prefixes.
diff --git a/plugins/context-mode/skills/ctx-index/SKILL.md b/plugins/context-mode/skills/ctx-index/SKILL.md
new file mode 100644
index 0000000..a50a1e5
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-index/SKILL.md
@@ -0,0 +1,45 @@
+---
+name: ctx-index
+description: |
+ Index a local file or directory into context-mode's persistent FTS5 knowledge base
+ so future context-mode__ctx_search calls can retrieve focused snippets without rereading raw files.
+ Trigger: /context-mode:ctx-index
+---
+
+# Context Mode Index
+
+Index local project content for later search.
+
+## Instructions
+
+1. Prefer the `context-mode__ctx_index` MCP tool when it is available.
+2. Ask for a path only if the user did not provide one and the current project root is ambiguous.
+3. Use `path`, not large inline `content`, so file bytes do not enter the conversation.
+4. For repository indexing, pass conservative bounds and a clear source label:
+
+```javascript
+context-mode__ctx_index({
+ path: ".",
+ source: "project:",
+ maxDepth: 5,
+ maxFiles: 200
+})
+```
+
+5. If MCP tools are unavailable, fall back to the CLI:
+
+```bash
+context-mode index . --source project:
+```
+
+6. Report the indexed source label, file count or section count, and the matching search command:
+
+```javascript
+context-mode__ctx_search({ source: "project:", queries: ["..."] })
+```
+
+## Safety
+
+- Do not index dependency directories, build outputs, secrets, or generated artifacts.
+- Prefer `--exclude` or `exclude` for project-specific noisy paths.
+- For broad repos, ask the user before raising `maxFiles` above 500.
diff --git a/plugins/context-mode/skills/ctx-insight/SKILL.md b/plugins/context-mode/skills/ctx-insight/SKILL.md
new file mode 100644
index 0000000..f5aa413
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-insight/SKILL.md
@@ -0,0 +1,28 @@
+---
+name: ctx-insight
+description: |
+ Open the context-mode Insight analytics dashboard in the browser.
+ Shows personal metrics: session activity, tool usage, error rate,
+ parallel work patterns, project focus, and actionable insights.
+ First run installs dependencies (~30s). Subsequent runs open instantly.
+ Trigger: /context-mode:ctx-insight
+---
+
+# Context Mode Insight
+
+Open the personal analytics dashboard in the browser.
+
+## Instructions
+
+1. Call the `context-mode__ctx_insight` MCP tool (no parameters needed, or pass `port: 4747` to customize). Optional data-dir overrides: `sessionDir`/`insightSessionDir` for `INSIGHT_SESSION_DIR`, and `contentDir`/`insightContentDir` for `INSIGHT_CONTENT_DIR`.
+2. The tool will:
+ - Copy source files to cache (first run only)
+ - Install dependencies (first run only, ~30s)
+ - Build the dashboard (~1s)
+ - Start a local server
+ - Open the browser
+3. Display the tool's output to the user β it contains progress steps and the dashboard URL.
+4. Tell the user:
+ - "Dashboard is running at http://localhost:4747"
+ - "Refresh the page to see updated metrics"
+ - "Dashboard stops automatically when ECA exits. To stop sooner: kill the PID shown above."
diff --git a/plugins/context-mode/skills/ctx-purge/SKILL.md b/plugins/context-mode/skills/ctx-purge/SKILL.md
new file mode 100644
index 0000000..9127c82
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-purge/SKILL.md
@@ -0,0 +1,48 @@
+---
+name: ctx-purge
+description: |
+ Purge the context-mode knowledge base. Permanently deletes all indexed content
+ and resets session stats. This is destructive and cannot be undone.
+ Trigger: /context-mode:ctx-purge
+---
+
+# Context Mode Purge
+
+Permanently deletes session data for this project. Two scopes are supported (issue #520):
+
+- **Project scope** (`scope: "project"`): wipes EVERYTHING β knowledge base, all session DB rows for every session, events markdown, and stats.
+- **Session scope** (`sessionId: ""` or `scope: "session"`): wipes ONLY the matching session's rows + FTS5 chunks. Sibling sessions, project stats, and the FTS5 store file are preserved.
+
+## Instructions
+
+1. **Decide the scope first** with the user:
+ - "Wipe just one session?" β ask for the `sessionId`.
+ - "Wipe the whole project?" β confirm scope:'project' (this is the destructive, irreversible default).
+2. **Warn the user about scope:'project'**. Everything will be deleted:
+ - FTS5 knowledge base (all indexed content from `context-mode__ctx_index`, `context-mode__ctx_fetch_and_index`, `context-mode__ctx_batch_execute`)
+ - Session events DB (analytics, metadata, resume snapshots) for ALL sessions in the project
+ - Session events markdown file
+ - In-memory session stats + persisted stats file
+3. Call the `context-mode__ctx_purge` MCP tool with the chosen parameters:
+ - Scoped: `{ confirm: true, sessionId: "" }` β implies scope:'session'.
+ - Project: `{ confirm: true, scope: "project" }` β explicit destructive form.
+ - Bare `{ confirm: true }` still works but emits a deprecation warning. Prefer the explicit forms.
+4. Report the result to the user β the response lists exactly what was deleted and (for scoped purges) confirms that other sessions and project stats were preserved.
+
+## Schema rules
+
+- `confirm: true` is always required.
+- `sessionId` and `scope: "project"` together is REJECTED as ambiguous (the sessionId implies session scope; combining with project scope contradicts intent).
+- `scope: "session"` without `sessionId` throws β sessionId is required.
+
+## When to Use
+
+- **Scoped (per-session)**: scratch acceptance scenarios, drill replays, isolating a polluted session without losing the main working session's stats.
+- **Project**: KB contains stale or incorrect content polluting search results, switching between unrelated projects in the same session, completely fresh start.
+
+## Important
+
+- `context-mode__ctx_purge` is the **only** way to delete session data. No other mechanism exists.
+- `context-mode__ctx_stats` is read-only β shows statistics only.
+- `/clear` and `/compact` do NOT affect any context-mode data.
+- There is no undo. Re-index content if you need it again.
diff --git a/plugins/context-mode/skills/ctx-search/SKILL.md b/plugins/context-mode/skills/ctx-search/SKILL.md
new file mode 100644
index 0000000..fe147ae
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-search/SKILL.md
@@ -0,0 +1,34 @@
+---
+name: ctx-search
+description: |
+ Search context-mode's persistent FTS5 knowledge base for previously indexed
+ local project content, documentation, or session memory.
+ Trigger: /context-mode:ctx-search
+---
+
+# Context Mode Search
+
+Search indexed content without rereading raw sources into conversation context.
+
+## Instructions
+
+1. Prefer the `context-mode__ctx_search` MCP tool when it is available.
+2. Batch all related questions in one `queries` array.
+3. Scope with `source` when the user names a project or indexed label.
+4. Use short, specific queries of two to four technical terms.
+
+```javascript
+context-mode__ctx_search({
+ source: "project:",
+ queries: ["authentication middleware", "token refresh"],
+ limit: 5
+})
+```
+
+5. If MCP tools are unavailable, fall back to the CLI:
+
+```bash
+context-mode search "authentication middleware" --source project: --limit 5
+```
+
+6. If the index is empty, tell the user to run `/context-mode:ctx-index` or `context-mode index ` first.
diff --git a/plugins/context-mode/skills/ctx-stats/SKILL.md b/plugins/context-mode/skills/ctx-stats/SKILL.md
new file mode 100644
index 0000000..40a3065
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-stats/SKILL.md
@@ -0,0 +1,25 @@
+---
+name: ctx-stats
+description: |
+ Show how much context window context-mode saved this session.
+ Displays token consumption, context savings ratio, and per-tool breakdown.
+ Read-only β shows stats only, no reset capability.
+ To wipe the knowledge base entirely, use context-mode__ctx_purge instead.
+ Trigger: /context-mode:ctx-stats
+---
+
+# Context Mode Stats
+
+Show context savings for the current session.
+
+## Instructions
+
+1. Call the `context-mode__ctx_stats` MCP tool (no parameters needed).
+2. **CRITICAL**: You MUST copy-paste the ENTIRE tool output as markdown text directly into your response message. Do NOT summarize, do NOT collapse, do NOT paraphrase. The user must see the full tables without pressing ctrl+o. Copy every line exactly as returned by the tool.
+3. After the full output, add ONE sentence highlighting the key savings metric, e.g.:
+ - "context-mode saved **12.4x** β 92% of data stayed in sandbox."
+ - If no data yet: "No context-mode calls yet this session."
+
+## Purge
+
+- **`context-mode__ctx_purge(confirm: true)`** β Permanently deletes all indexed content from the knowledge base. Use `/context-mode:ctx-purge` for this.
diff --git a/plugins/context-mode/skills/ctx-upgrade/SKILL.md b/plugins/context-mode/skills/ctx-upgrade/SKILL.md
new file mode 100644
index 0000000..d8345be
--- /dev/null
+++ b/plugins/context-mode/skills/ctx-upgrade/SKILL.md
@@ -0,0 +1,30 @@
+---
+name: ctx-upgrade
+description: |
+ Update context-mode from GitHub and fix hooks/settings.
+ Pulls latest, builds, installs, updates npm global, configures hooks.
+ Trigger: /context-mode:ctx-upgrade
+---
+
+# Context Mode Upgrade
+
+Pull latest from GitHub and reinstall the plugin.
+
+## Instructions
+
+1. Call the `context-mode__ctx_upgrade` MCP tool directly. It returns a shell command to execute.
+2. Run the returned command using `eca__shell_command`.
+3. Display results as a markdown checklist:
+ ```
+ ## context-mode upgrade
+ - [x] Pulled latest from GitHub
+ - [x] Built and installed v1.0.39
+ - [x] Hooks configured
+ - [x] Doctor: all checks PASS
+ ```
+ Use `[x]` for success, `[ ]` for failure. Show actual version numbers.
+4. Tell the user to **restart their session** to pick up the new version.
+5. **Fallback** (only if MCP tool call fails): run the installed CLI with `eca__shell_command`:
+ ```bash
+ context-mode upgrade
+ ```