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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ countries and as low as $100 a year. <a href="https://app.harnessmd.com/console/
> [!NOTE]
> **The world's best agents. The world's worst paper company.**
> Munder Difflin takes the terminal-agent CLIs you already run (`claude`, `agy`, `codex`, `grok`,
> `kimi`, `qwen`, `opencode`, `crush`, `pi`, and `copilot`) and turns them
> `kimi`, `qwen`, `opencode`, `crush`, `pi`, `copilot`, and `devin`) and turns them
> into a self-coordinating team: each agent gets long-term memory, a mailbox, and a desk on a 2D
> office floor, and **your clone** (Michael) routes work between them while you watch. He's the
> boss of the floor; you're still the boss of him.
Expand Down Expand Up @@ -116,7 +116,7 @@ wires them into a **hive mind**, and puts **your clone** in charge. Michael is t
talk to in order to get things done. Under the hood it runs the **fastest memory layer in the
world** so every agent remembers what it learns and recalls it instantly.

- **Every terminal is an agent.** Each `claude`, `agy`, `codex`, `grok`, `kimi`, `qwen`, `opencode`, `crush`, `pi`, `copilot`, or custom session runs as a real
- **Every terminal is an agent.** Each `claude`, `agy`, `codex`, `grok`, `kimi`, `qwen`, `opencode`, `crush`, `pi`, `copilot`, `devin`, or custom session runs as a real
process in a pseudo-terminal (`node-pty`), byte-for-byte authentic, rendered with xterm.js.
- **Every agent is an avatar.** Sessions appear as characters on a Pixi.js office floor. They walk
to stations as they work, and envelopes fly desk-to-desk when they message each other.
Expand Down Expand Up @@ -398,7 +398,8 @@ Everything below is for contributors and for people who want to run an unrelease
- At least one supported agent CLI on your `PATH`, one of **[Claude Code](https://claude.com/claude-code)**
(`claude`, the default), **Antigravity** (`agy`), **OpenAI Codex** (`codex`), **xAI Grok** (`grok`),
**Kimi Code** (`kimi`), **Gemini CLI** (`gemini`), **Qwen** (`qwen`), **OpenCode** (`opencode`),
**Crush** (`crush`), **pi.dev** (`pi`), **GitHub Copilot** (`copilot`), or **Cursor** (`cursor-agent`).
**Crush** (`crush`), **pi.dev** (`pi`), **GitHub Copilot** (`copilot`), **Cursor** (`cursor-agent`),
or **[Devin CLI](https://docs.devin.ai/cli)** (`devin`).
Most missing CLIs self-heal: the harness runs the installer in the
terminal and continues into the new binary.
- *Optional:* **your own API keys and local LLMs** in **Settings → AI Engines** (Ollama / LM Studio / vLLM).
Expand All @@ -416,6 +417,18 @@ npm run dev # launches the Electron app with hot reload
On first launch you'll go through the onboarding wizard, then land on the floor. Use **Add agent** to
spawn your first session. The GOD agent seats itself in Michael's office automatically.

To use Devin, select **Devin CLI** in Add agent and choose a SWE-2 model. The command field is
editable, so you can add a personal prompt file, for example:

```text
devin --model swe-2 --permission-mode dangerous --prompt-file /path/to/DEVIN.md
```

The hive briefing is passed as Devin's initial interactive prompt. If you set `--prompt-file`,
the app copies its contents and the hive briefing into a private per-agent prompt file because
Devin rejects a prompt file together with an inline prompt. Your original file is untouched.
Auto mode supplies `--permission-mode dangerous` when enabled.

### Other scripts

```bash
Expand Down
16 changes: 16 additions & 0 deletions docs/model-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@
{ "id": "claude-opus-4-8-high", "label": "Opus 4.8 1M", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-sonnet-5-thinking-high", "label": "Sonnet 5 1M Thinking", "minAppVersion": null, "maxAppVersion": null }
],
"devin": [
{ "label": "CLI default", "minAppVersion": null, "maxAppVersion": null },
{ "id": "adaptive", "label": "Adaptive", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2", "label": "SWE-2", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2-high", "label": "SWE-2 High", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2-medium", "label": "SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2-max", "label": "SWE-2 Max", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-opus-5-5-medium", "label": "Opus 5.5 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-fable-5-1-medium", "label": "Fable 5.1 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "gpt-6-sol-medium", "label": "GPT-6 Sol Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "gpt-6-astra-medium", "label": "GPT-6 Astra Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-claude-opus-5-5-high-sidekick-swe-2-medium", "label": "Fusion · Opus 5.5 High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-claude-fable-5-1-high-sidekick-swe-2-medium", "label": "Fusion · Fable 5.1 High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-gpt-6-sol-high-sidekick-swe-2-medium", "label": "Fusion · GPT-6 Sol High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-gpt-6-astra-high-sidekick-swe-2-medium", "label": "Fusion · GPT-6 Astra High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null }
],
"custom": []
}
}
Binary file added evidence/devin-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added evidence/devin-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/main/devinPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { isAbsolute, join, resolve } from 'node:path';
import { homedir } from 'node:os';

/** Devin rejects --prompt-file together with a positional initial prompt. When
* the user supplied a prompt file, keep its contents and the hive briefing in
* one private file, then pass only --prompt-file to Devin. The user's file is
* never modified. Without a prompt file, leave `-- <hive prompt>` intact. */
export function prepareDevinPromptFile(args: string[], cwd: string, agentDir: string): string[] {
const separator = args.lastIndexOf('--');
if (separator < 0 || separator !== args.length - 2) return args;

let fileArg = -1;
let sourcePath = '';
let equalsForm = false;
for (let i = 0; i < separator; i++) {
if (args[i] === '--prompt-file' && i + 1 < separator) {
fileArg = i + 1;
sourcePath = args[i + 1];
equalsForm = false;
i++;
} else if (args[i].startsWith('--prompt-file=')) {
fileArg = i;
sourcePath = args[i].slice('--prompt-file='.length);
equalsForm = true;
}
}
if (fileArg < 0) return args;

const expanded = sourcePath === '~' ? homedir() : sourcePath.startsWith('~/') ? join(homedir(), sourcePath.slice(2)) : sourcePath;
const source = isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
const personalPrompt = readFileSync(source, 'utf8');
const privateDir = join(agentDir, '.devin');
mkdirSync(privateDir, { recursive: true, mode: 0o700 });
const combinedPath = join(privateDir, 'initial-prompt.md');
writeFileSync(combinedPath, `${personalPrompt.trimEnd()}\n\n${args[separator + 1]}\n`, { mode: 0o600 });

const result = args.slice(0, separator);
result[fileArg] = equalsForm ? `--prompt-file=${combinedPath}` : combinedPath;
return result;
}
2 changes: 1 addition & 1 deletion src/main/hive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ function shortRand(): string {
const PROXY_BIND_ATTEMPTS = 3;
const PROXY_BIND_BACKOFF_MS = [250, 750];

const MINE_IGNORE_LINES = ['settings.json', 'cursor.json', 'inbox/', 'outbox/', '.codex/'];
const MINE_IGNORE_LINES = ['settings.json', 'cursor.json', 'inbox/', 'outbox/', '.codex/', '.devin/'];

/** Idempotently ensure `<agentDir>/.gitignore` excludes the non-memory files.
* Append-only: writes only the missing lines, leaving any existing entries. */
Expand Down
19 changes: 17 additions & 2 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
type AgentProvider
} from '../shared/agentProvider';
import { buildMissingCliScript, chooseInstallRung } from './cliInstall';
import { prepareDevinPromptFile } from './devinPrompt';
import { detectNodeVersion, nodeIsUsable, resolveNodeInstaller } from './nodeInstall';
import { toolCatalog, type ToolStatus } from '../shared/toolCatalog';
import { listLocalSkills, loadCatalog, installSkill, uninstallSkill, type LocalSkill } from './skills';
Expand Down Expand Up @@ -2775,6 +2776,11 @@ async function spawnAgentCore(opts: AgentSpawnOptions, owner: Electron.WebConten
// seedDelivery:'type-into-tui') rather than passed on argv. Surfaced in the spawn
// result so the renderer types it through the per-pty write-chain. (ondev-b)
let seedPrompt: string | undefined;
// Devin uses `-- <initial prompt>`; all CLI options must precede that
// separator. Main-only hires also need the same auto-mode posture as GUI hires.
if (provider === 'devin') {
opts.args = argsWithAutoModeFlag(opts.args ?? [], readConfig().autoMode, provider);
}
if (opts.hive && hive.enabled()) {
try {
const inj = await hive.ensureAgent(
Expand All @@ -2796,7 +2802,10 @@ async function spawnAgentCore(opts: AgentSpawnOptions, owner: Electron.WebConten
extraWritableDirs: [memory.env().MEMPALACE_PALACE_PATH].filter((p): p is string => !!p)
}
);
opts.args = [...(opts.args ?? []), ...inj.args];
const injectedArgs = [...(opts.args ?? []), ...inj.args];
opts.args = provider === 'devin'
? prepareDevinPromptFile(injectedArgs, opts.cwd, inj.env.AGENT_DIR)
: injectedArgs;
seedPrompt = inj.seedPrompt;
// A degraded spawn (proxy bridge never bound) is told to the user the same
// way breaker escalations are: a native toast, gated on the notifications
Expand Down Expand Up @@ -2905,7 +2914,13 @@ async function spawnAgentCore(opts: AgentSpawnOptions, owner: Electron.WebConten
const sid = typedSid || (opts.resume === true ? hive.lastSession(opts.hive.id) : undefined);
if (sid && rf) {
const args = opts.args ?? [];
if (!args.includes(rf)) { args.push(rf, sid); opts.args = args; didResume = true; }
if (!args.includes(rf)) {
const promptSeparator = preset.initialPromptFlag === '--' ? args.indexOf('--') : -1;
if (promptSeparator >= 0) args.splice(promptSeparator, 0, rf, sid);
else args.push(rf, sid);
opts.args = args;
didResume = true;
}
} else if (sid && rsub) {
// Subcommand form (Codex): `codex resume [OPTIONS] [SESSION_ID]` — the
// subcommand MUST be argv[0], the id trails the flags. Codex indexes
Expand Down
2 changes: 1 addition & 1 deletion src/main/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { quarantineDirsToReap, quarantineStampMs, nextMineDelayMs } from './pala
* that are not currently running. See hive.ts for why `.codex/` matters beyond
* mempalace: it is also what stopped the hive's git repo from versioning every
* Codex transcript and sqlite log into a 7.5GB history. */
const MINE_IGNORE_LINES = ['settings.json', 'cursor.json', 'inbox/', 'outbox/', '.codex/'];
const MINE_IGNORE_LINES = ['settings.json', 'cursor.json', 'inbox/', 'outbox/', '.codex/', '.devin/'];

/** Idempotently ensure `<agentDir>/.gitignore` excludes the non-memory files.
* Writes only the missing lines (append-only) so it's safe to call every cycle. */
Expand Down
2 changes: 1 addition & 1 deletion src/main/realtimeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const PENDING_TTL_MS = 120_000;
const PROVIDER_COMMAND: Record<string, string> = {
claude: 'claude', codex: 'codex', antigravity: 'antigravity', gemini: 'gemini',
opencode: 'opencode', crush: 'crush', pi: 'pi', qwen: 'qwen', copilot: 'copilot',
cursor: 'cursor-agent'
cursor: 'cursor-agent', devin: 'devin'
};

/** Bare affirmations that must NEVER authorize a destructive op on their own —
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/components/AddAgentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,8 @@ export function AddAgentModal({ onClose, config, onConfigChange }: AddAgentModal
? 'agy'
: provider === 'codex'
? 'codex'
: provider === 'devin'
? 'devin'
: provider === 'custom'
? 'your-agent-cli'
: 'claude'
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/components/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ const PROVIDER_BLURB_KEYS: Partial<Record<AgentProvider, string>> = {
codex: 'onboarding.providerBlurb.codex',
antigravity: 'onboarding.providerBlurb.antigravity',
qwen: 'onboarding.providerBlurb.qwen',
cursor: 'onboarding.providerBlurb.cursor'
cursor: 'onboarding.providerBlurb.cursor',
devin: 'onboarding.providerBlurb.devin'
};

export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,8 @@
"antigravity": "Antigravity — Google Gemini",
"qwen": "Qwen — runs a local Qwen model on your machine",
"gemini": "Gemini CLI - Google Gemini",
"cursor": "Cursor Agent CLI — uses your Cursor credits (Luna, Composer, …)"
"cursor": "Cursor Agent CLI — uses your Cursor credits (Luna, Composer, …)",
"devin": "Devin CLI — local agent with SWE-2 models"
},
"home": {
"descPlain": "Create a new, empty folder for the app to call home. Everything the app remembers — its own settings and your agents' memory — is stored here. Something like ~/HarnessAgents works well. We'll create it for you if it doesn't exist.",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/realtime/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function realtimeActionTools(): ReturnType<typeof tool>[] {
parameters: {
type: 'object',
properties: {
provider: { type: 'string', description: 'Engine: claude (default), codex, gemini, opencode, crush, pi, qwen, copilot, cursor.' },
provider: { type: 'string', description: 'Engine: claude (default), codex, gemini, opencode, crush, pi, qwen, copilot, cursor, devin.' },
role: { type: 'string', description: 'Optional. The role/job for the new agent.' },
name: { type: 'string', description: 'Optional. A name for the agent.' },
cwd: { type: 'string', description: 'Optional. Working directory; defaults to the hive root.' }
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/store/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ export type { CatalogModel, ModelCatalog } from '@shared/modelCatalogPayload';
* - cursor: ids match `cursor-agent models` / `--model` (Cursor account catalog).
* Luna is the cheap, high-context default for Michael; the rest are curated
* quick-picks and the command field stays editable for any live slug.
* - devin: `devin models list` accepts the SWE-2 family slug or its specific
* variants. The command field also accepts the user's `--prompt-file` flag.
* - grok: the models reported by the installed Grok CLI (`grok models`).
* - kimi: managed Kimi Code aliases accepted by `kimi --model <alias>`.
* - custom: no presets at all; the command field is the whole interface.
Expand Down
33 changes: 31 additions & 2 deletions src/shared/agentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type AgentProvider =
| 'pi'
| 'copilot'
| 'cursor'
| 'devin'
| 'custom';

/** Structured descriptor for how a NON-hiveAware provider gets hive lifecycle
Expand Down Expand Up @@ -567,6 +568,29 @@ export const AGENT_PROVIDER_PRESETS: AgentProviderPreset[] = [
},
docsUrl: 'https://cursor.com/docs/cli/install'
},
{
id: 'devin',
label: 'Devin CLI',
defaultCommand: 'devin',
commandGroups: [],
// Devin's interactive CLI accepts the hive briefing after `--` as its
// initial prompt. It remains open for subsequent turns and inbox nudges.
autoModeFlag: '--permission-mode dangerous',
autoFlag: '--permission-mode dangerous',
autoStanceTokens: ['--permission-mode', '--sandbox'],
supportsModel: true,
modelFlag: '--model',
hiveAware: false,
canReceiveInbox: true,
initialPromptFlag: '--',
recommendedOrchestratorModel: 'swe-2',
resumeFlag: '--resume',
nativeInstallCommand: {
posix: 'curl -fsSL https://cli.devin.ai/install.sh | bash',
win32: 'powershell -c irm https://static.devin.ai/cli/setup.ps1 ^| iex'
},
docsUrl: 'https://docs.devin.ai/cli'
},
{
id: 'custom',
label: 'Custom',
Expand Down Expand Up @@ -594,6 +618,7 @@ export function isAgentProvider(value: unknown): value is AgentProvider {
value === 'pi' ||
value === 'copilot' ||
value === 'cursor' ||
value === 'devin' ||
value === 'custom'
);
}
Expand Down Expand Up @@ -633,8 +658,11 @@ function commandBinary(command: string | undefined): string {
/** Infer the provider from a command (or honor an explicit override). */
export function inferAgentProvider(command: string | undefined, explicit?: unknown): AgentProvider {
const normalized = normalizeAgentProvider(explicit);
if (normalized) return normalized;
const bin = commandBinary(command);
// Older installs stored Devin commands as explicit `custom` agents because
// Devin had no preset. Recognise those on restore so existing agents inherit
// the model, prompt-file and permission handling without being recreated.
if (normalized && !(normalized === 'custom' && bin === 'devin')) return normalized;
if (bin === 'codex') return 'codex';
if (bin === 'grok') return 'grok';
if (bin === 'kimi') return 'kimi';
Expand All @@ -645,6 +673,7 @@ export function inferAgentProvider(command: string | undefined, explicit?: unkno
if (bin === 'crush') return 'crush';
if (bin === 'pi') return 'pi';
if (bin === 'copilot') return 'copilot';
if (bin === 'devin') return 'devin';
// Cursor ships as `cursor-agent`; `agent` is a shorter alias (generic name — check last).
if (bin === 'cursor-agent') return 'cursor';
if (bin === 'agent') return 'cursor';
Expand Down Expand Up @@ -699,7 +728,7 @@ export function hasAutoModeStance(args: string[], provider: AgentProvider): bool
const flag = preset.autoModeFlag ?? '';
const lead = flag.trim().split(/\s+/)[0];
const stance = new Set([...(lead ? [lead] : []), ...(preset.autoStanceTokens ?? [])]);
return args.some((a) => stance.has(a));
return args.some((a) => stance.has(a.split('=', 1)[0]));
}

/** Returns any env vars the provider needs for non-interactive / first-run suppression. */
Expand Down
16 changes: 16 additions & 0 deletions src/shared/modelCatalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@
{ "id": "claude-opus-4-8-high", "label": "Opus 4.8 1M", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-sonnet-5-thinking-high", "label": "Sonnet 5 1M Thinking", "minAppVersion": null, "maxAppVersion": null }
],
"devin": [
{ "label": "CLI default", "minAppVersion": null, "maxAppVersion": null },
{ "id": "adaptive", "label": "Adaptive", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2", "label": "SWE-2", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2-high", "label": "SWE-2 High", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2-medium", "label": "SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "swe-2-max", "label": "SWE-2 Max", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-opus-5-5-medium", "label": "Opus 5.5 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-fable-5-1-medium", "label": "Fable 5.1 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "gpt-6-sol-medium", "label": "GPT-6 Sol Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "gpt-6-astra-medium", "label": "GPT-6 Astra Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-claude-opus-5-5-high-sidekick-swe-2-medium", "label": "Fusion · Opus 5.5 High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-claude-fable-5-1-high-sidekick-swe-2-medium", "label": "Fusion · Fable 5.1 High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-gpt-6-sol-high-sidekick-swe-2-medium", "label": "Fusion · GPT-6 Sol High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null },
{ "id": "fusion-gpt-6-astra-high-sidekick-swe-2-medium", "label": "Fusion · GPT-6 Astra High + SWE-2 Medium", "minAppVersion": null, "maxAppVersion": null }
],
"custom": []
}
}
Loading
Loading