Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
03e8315
ci: update server.bundle.mjs, cli.bundle.mjs, session hook & security…
github-actions[bot] Jun 29, 2026
4dedadc
ci: update install stats
github-actions[bot] Jun 29, 2026
218a4fc
ci: update install stats
github-actions[bot] Jun 30, 2026
8d68571
ci: update install stats
github-actions[bot] Jun 30, 2026
a0b4609
ci: update install stats
github-actions[bot] Jul 1, 2026
3fbc6ea
ci: update install stats
github-actions[bot] Jul 1, 2026
40f2332
ci: update install stats
github-actions[bot] Jul 2, 2026
ee78b02
ci: update install stats
github-actions[bot] Jul 2, 2026
f3f15e1
ci: update install stats
github-actions[bot] Jul 3, 2026
429d6f2
ci: update install stats
github-actions[bot] Jul 3, 2026
c3fb920
ci: update install stats
github-actions[bot] Jul 4, 2026
aa4e965
ci: update install stats
github-actions[bot] Jul 4, 2026
4f60c87
ci: update install stats
github-actions[bot] Jul 5, 2026
f267bda
ci: update install stats
github-actions[bot] Jul 6, 2026
475dc23
ci: update install stats
github-actions[bot] Jul 7, 2026
d0a0eeb
ci: update install stats
github-actions[bot] Jul 7, 2026
477d845
ci: update install stats
github-actions[bot] Jul 8, 2026
43a2066
ci: update install stats
github-actions[bot] Jul 8, 2026
c3eb1bc
feat: add Devin CLI adapter + gate Claude-Code self-heal for non-CC p…
alexnimo Jul 9, 2026
c8a1da9
feat: add getHealthChecks override to DevinAdapter
alexnimo Jul 14, 2026
a0d7550
test: add integration tests for Devin CLI hook scripts
alexnimo Jul 14, 2026
517df43
feat: port decision extraction from token-optimizer to Devin Stop hook
alexnimo Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 180 additions & 178 deletions cli.bundle.mjs

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions configs/devin/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "exec|read|grep|webfetch|web_search|edit|write|Bash|Read|Grep|WebFetch|Edit|Write|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__",
"hooks": [
{ "type": "command", "command": "context-mode hook devin pretooluse" }
]
}
],
"PostToolUse": [
{
"hooks": [
{ "type": "command", "command": "context-mode hook devin posttooluse" }
]
}
],
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "context-mode hook devin sessionstart" }
]
}
],
"PreCompact": [
{
"hooks": [
{ "type": "command", "command": "context-mode hook devin precompact" }
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "context-mode hook devin userpromptsubmit" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "context-mode hook devin stop" }
]
}
]
}
}
11 changes: 11 additions & 0 deletions configs/devin/mcp_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"mcpServers": {
"context-mode": {
"command": "context-mode",
"transport": "stdio",
"env": {
"CONTEXT_MODE_PLATFORM": "devin"
}
}
}
}
45 changes: 45 additions & 0 deletions hooks/core/formatters.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,51 @@ export const formatters = {
agent_message: additionalContext,
}),
},

// Devin CLI uses Claude-Code-compatible hookSpecificOutput format.
// updatedInput (modify) is not documented for Devin; convert modify→deny
// carrying the redirect guidance (same approach as Codex <0.141.0 and
// antigravity-cli). additionalContext is honored via hookSpecificOutput.
// On Windows, Devin's hook runner cannot spawn commands (confirmed bug
// as of Devin 2026.7.23) — these formatters are no-ops there until
// Cognition fixes the spawn mechanism.
"devin": {
deny: (reason) => ({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: reason,
},
}),
// Devin has no "ask" concept in hook output — drop it.
ask: () => null,
// Devin does not document updatedInput. Convert modify to deny carrying
// the redirect guidance (mirrors codex/antigravity-cli fail-closed).
modify: (updatedInput) => {
const ui = updatedInput ?? {};
// Only command redirects must fail closed. Non-command rewrites
// (e.g. Agent prompt injection) are advisory — drop rather than block.
if (!("command" in ui)) return null;
const cmd = ui.command ?? "";
const m = String(cmd).match(/^echo\s+"([\s\S]*)"\s*$/);
const guidance = m ? m[1].replace(/\\(["\\])/g, "$1") : "";
return {
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason:
guidance ||
"context-mode: command redirected. Use the context-mode MCP tools (ctx_execute / ctx_fetch_and_index / ctx_search) so raw output stays out of the conversation.",
},
};
},
context: (additionalContext) => ({
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext,
},
}),
},
};

// Keep in sync with the identical agyContextReason in
Expand Down
3 changes: 3 additions & 0 deletions hooks/core/platform-detect.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const PLATFORM_ENV_VARS_MIRROR = [
["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY"]],
["qwen-code", ["QWEN_PROJECT_DIR"]],
["pi", ["PI_PROJECT_DIR"]],
// devin — no auto-set process env vars (verified by env dump probe).
// Detection relies on CONTEXT_MODE_PLATFORM=devin override or MCP
// clientInfo.name from the initialize handshake. Falls through to default.
// openclaw — no auto-set process env vars; falls through to default
// kiro — no auto-set process env vars; falls through to default
];
Expand Down
8 changes: 8 additions & 0 deletions hooks/core/routing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,14 @@ const TOOL_ALIASES = {
"fs_read": "Read",
"fs_write": "Write",
"execute_bash": "Bash",
// Devin CLI native tool names
"exec": "Bash",
"read": "Read",
"grep": "Grep",
"webfetch": "WebFetch",
"web_search": "WebSearch",
"edit": "Edit",
"write": "Write",
};

function toolLeafName(toolName) {
Expand Down
2 changes: 2 additions & 0 deletions hooks/core/tool-naming.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* | Kiro | @context-mode/<tool> |
* | Zed | mcp:context-mode:<tool> |
* | Cursor / Codex / OpenClaw / Pi | bare <tool> |
* | Devin CLI | bare <tool> |
*/

const TOOL_PREFIXES = {
Expand All @@ -34,6 +35,7 @@ const TOOL_PREFIXES = {
"openclaw": (tool) => tool,
"pi": (tool) => tool,
"qwen-code": (tool) => `mcp__context-mode__${tool}`,
"devin": (tool) => tool,
};

/**
Expand Down
1 change: 1 addition & 0 deletions hooks/devin/platform.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
process.env.CONTEXT_MODE_PLATFORM = "devin";
71 changes: 71 additions & 0 deletions hooks/devin/posttooluse.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
import "./platform.mjs";
import "../suppress-stderr.mjs";
import "../ensure-deps.mjs";
/**
* Devin CLI postToolUse hook — session event capture.
*/

import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, DEVIN_OPTS } from "../session-helpers.mjs";
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
const OPTS = DEVIN_OPTS;

function normalizeToolName(toolName) {
// Normalize Devin-native tool names to canonical names for routing.
if (toolName === "exec") return "Bash";
if (toolName === "read") return "Read";
if (toolName === "grep") return "Grep";
if (toolName === "webfetch") return "WebFetch";
if (toolName === "web_search") return "WebSearch";
if (toolName === "edit") return "Edit";
if (toolName === "write") return "Write";
return toolName;
}

try {
const raw = await readStdin();
const input = parseStdin(raw);
const projectDir = getInputProjectDir(input, OPTS);

const { extractEvents } = await loadExtract();
const { resolveProjectAttributions } = await loadProjectAttribution();
const { SessionDB } = await loadSessionDB();

const dbPath = getSessionDBPath(OPTS, projectDir);
const db = new SessionDB({ dbPath });
const sessionId = getSessionId(input, OPTS);

db.ensureSession(sessionId, projectDir);

const normalizedInput = {
tool_name: normalizeToolName(input.tool_name ?? ""),
tool_input: input.tool_input ?? {},
tool_response: typeof input.tool_response === "string"
? input.tool_response
: JSON.stringify(input.tool_response ?? ""),
tool_output: input.tool_output
? {
...input.tool_output,
isError: input.tool_output.isError === true || input.tool_output.is_error === true,
}
: undefined,
};

const events = extractEvents(normalizedInput);

attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);

db.close();
} catch {
// Swallow errors — hook must not fail
}

// Devin uses Claude-Code-compatible hookSpecificOutput format
process.stdout.write(JSON.stringify({
hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: "" },
}) + "\n");
73 changes: 73 additions & 0 deletions hooks/devin/precompact.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env node
import "./platform.mjs";
import "../suppress-stderr.mjs";
import "../ensure-deps.mjs";
/**
* Devin CLI PreCompact hook - snapshot generation.
*
* Devin emits PostCompaction (not PreCompact). We map this hook to fire
* on the compaction event to capture a snapshot before context is lost.
*/

import {
readStdin,
parseStdin,
getSessionId,
getSessionDBPath,
getInputProjectDir,
resolveConfigDir,
DEVIN_OPTS,
} from "../session-helpers.mjs";
import { createSessionLoaders } from "../session-loaders.mjs";
import { appendFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
const OPTS = DEVIN_OPTS;
const DEBUG_LOG = join(resolveConfigDir(OPTS), "context-mode", "precompact-debug.log");

try {
const raw = await readStdin();
const input = parseStdin(raw);
const projectDir = getInputProjectDir(input, OPTS);

const { buildResumeSnapshot } = await loadSnapshot();
const { SessionDB } = await loadSessionDB();

const dbPath = getSessionDBPath(OPTS, projectDir);
const db = new SessionDB({ dbPath });
const sessionId = getSessionId(input, OPTS);

const events = db.getEvents(sessionId);

if (events.length > 0) {
const stats = db.getSessionStats(sessionId);
const snapshot = buildResumeSnapshot(events, {
compactCount: (stats?.compact_count ?? 0) + 1,
});

db.upsertResume(sessionId, snapshot, events.length);
db.incrementCompactCount(sessionId);

const fileEvents = events.filter((event) => event.category === "file");
db.insertEvent(sessionId, {
type: "compaction_summary",
category: "compaction",
data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
priority: 1,
}, "PreCompact");
}

db.close();
} catch (err) {
try {
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${err?.message || err}\n`);
} catch {
// Hook errors must not break Devin compaction.
}
}

// Devin PreCompact accepts universal hook fields only; no hookSpecificOutput.
process.stdout.write(JSON.stringify({}) + "\n");
37 changes: 37 additions & 0 deletions hooks/devin/pretooluse.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node
import "./platform.mjs";
import "../suppress-stderr.mjs";
/**
* Devin CLI preToolUse hook for context-mode.
*
* Devin PreToolUse honors `permissionDecision:"deny"` (Claude-Code-compatible
* format). updatedInput (modify) is not documented for Devin; we convert
* modify→deny+context for safety (same approach as Codex <0.141.0).
* additionalContext injection is supported via hookSpecificOutput.
*
* NOTE: On Windows, Devin's hook runner cannot spawn commands (confirmed
* bug as of Devin 2026.7.23). This hook is a no-op on Windows until
* Cognition fixes the spawn mechanism. The MCP server still works.
*/

import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { readStdin, parseStdin, getInputProjectDir, getSessionId, DEVIN_OPTS } from "../session-helpers.mjs";
import { routePreToolUse, initSecurity } from "../core/routing.mjs";
import { formatDecision } from "../core/formatters.mjs";

const __hookDir = dirname(fileURLToPath(import.meta.url));
await initSecurity(resolve(__hookDir, "..", "..", "build"));

const raw = await readStdin();
const input = parseStdin(raw);
const tool = input.tool_name ?? "";
const toolInput = input.tool_input ?? {};
const projectDir = getInputProjectDir(input, DEVIN_OPTS);

const decision = routePreToolUse(tool, toolInput, projectDir, "devin", getSessionId(input, DEVIN_OPTS));
const response = formatDecision("devin", decision);
const output = response ?? {
hookSpecificOutput: { hookEventName: "PreToolUse" },
};
process.stdout.write(JSON.stringify(output) + "\n");
Loading