Skip to content
Merged
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
8 changes: 4 additions & 4 deletions src/main/hive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,8 +791,7 @@ export class HiveManager {

const claudeProvider = isClaudeProvider(meta.provider ?? 'claude');

// Non-hive-aware providers (Antigravity's `agy`, OpenAI's `codex`, xAI's
// `grok`) don't
// Non-hive-aware providers (for example Antigravity, Codex, Grok and Pi) don't
// understand Claude Code's flags (no `--append-system-prompt`, no telemetry,
// no `--settings`). Instead: (1) the hive identity+protocol rides in as the
// session's INITIAL prompt — the closest thing to `--append-system-prompt`
Expand All @@ -803,7 +802,7 @@ export class HiveManager {
//
// How the prompt rides in differs by CLI:
// - agy takes it under a flag (`agy -i "<prompt>"`) → push [flag, prompt].
// - codex/grok take it POSITIONALLY (`codex|grok "<prompt>"`) → push the
// - codex/grok/pi take it POSITIONALLY (`codex|grok|pi "<prompt>"`) → push the
// bare prompt as a trailing arg (node-pty passes argv literally, so it
// arrives as one positional argument after codex's own flags).
if (!isHiveAwareProvider(meta.provider)) {
Expand Down Expand Up @@ -930,7 +929,8 @@ export class HiveManager {
// seedPrompt; the renderer types it into the TUI after boot (ondev-b).
const deg = degraded ? { degraded } : {};
if (preset.seedDelivery === 'type-into-tui') return { args: [...preArgs], env, seedPrompt: prompt, ...deg };
// If a provider somehow exposes neither a flag nor a positional prompt, spawn bare.
// Providers with no declared seed strategy intentionally spawn bare. Inbox-capable
// non-hive-aware presets are guarded by the provider contract tests.
if (flag) return { args: [...preArgs, flag, prompt], env, ...deg };
if (preset.positionalInitialPrompt) return { args: [...preArgs, prompt], env, ...deg };
return { args: preArgs, env, ...deg };
Expand Down
15 changes: 9 additions & 6 deletions src/shared/agentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export interface AgentProviderPreset {
/** For non-hive-aware CLIs that still take an INITIAL prompt to orient the
* session (Antigravity's `agy -i "<prompt>"`), the flag to pass it under. The
* hive identity+protocol rides in as the first turn — the closest thing to
* Claude's `--append-system-prompt` these CLIs offer. undefined = the CLI
* takes its initial prompt POSITIONALLY (Codex: `codex "<prompt>"`) and the
* injection branch appends it as a quoted trailing arg instead of a flag. */
* Claude's `--append-system-prompt` these CLIs offer. undefined means this
* provider has no flag form; positional delivery must be declared explicitly
* with `positionalInitialPrompt`. */
initialPromptFlag?: string;
/** How the hive protocol seed is delivered for a CLI that takes NEITHER a flag
* nor a positional seed. `'type-into-tui'` = the CLI is a bare interactive TUI
Expand All @@ -135,8 +135,8 @@ export interface AgentProviderPreset {
* collide). Absent/undefined = today's flag-or-positional behavior. (ondev-b) */
seedDelivery?: 'type-into-tui';
/** This CLI accepts the initial hive prompt as a trailing positional argument.
* Codex does; Kimi/custom do not, so they must spawn bare when no prompt flag
* exists instead of receiving an invalid positional argument. */
* Codex/Grok/Pi do; Kimi/custom do not, so they must spawn bare when no prompt
* flag exists instead of receiving an invalid positional argument. */
positionalInitialPrompt?: boolean;
/** Flag to resume a prior session on respawn, given the recorded session id
* (Claude `--resume <sid>`, Antigravity `--conversation <id>`). undefined = no
Expand Down Expand Up @@ -489,7 +489,10 @@ export const AGENT_PROVIDER_PRESETS: AgentProviderPreset[] = [
// or we lean on the renderer idle nudge) is UNVERIFIED pending keys. Renderer nudge
// is the guaranteed drain fallback either way.
canReceiveInbox: true,
initialPromptFlag: undefined, // positional, like codex: pi "<prompt>"
initialPromptFlag: undefined,
// Pi's documented `pi [options] [messages...]` form accepts the hive protocol
// as its initial user message while leaving the interactive session alive.
positionalInitialPrompt: true,
resumeFlag: '--session',
// --ignore-scripts: don't run the package's postinstall on the user's machine.
installCommand: 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent',
Expand Down
60 changes: 60 additions & 0 deletions test/hive-pi-bootstrap.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const loadTs = require('./load-ts.cjs');

const { HiveManager } = loadTs('src/main/hive.ts');

function tmpHome() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'md-hive-pi-bootstrap-'));
}

test('Pi fresh spawn receives exactly one positional hive bootstrap', async (t) => {
const home = tmpHome();
t.after(() => fs.rmSync(home, { recursive: true, force: true }));

const hive = new HiveManager(() => home);
const injection = await hive.ensureAgent({
id: 'toby-pi-test',
name: 'Toby',
provider: 'pi',
cwd: home
});

assert.equal(injection.seedPrompt, undefined, 'Pi receives its bootstrap on argv, not via TUI typing');
assert.equal(injection.args.length, 1, 'the bootstrap must remain one trailing positional argument');

const [prompt] = injection.args;
assert.match(prompt, /^You are "Toby" \(toby-pi-test\),/);
assert.match(prompt, /HIVE PROTOCOL/);
assert.ok(prompt.includes(path.join(home, 'hive', 'agents', 'toby-pi-test', 'inbox')));
assert.ok(prompt.split('\n').length > 5, 'the multiline bootstrap must not be split into argv tokens');
});

test('Pi bridge setup failure does not suppress its positional bootstrap', async (t) => {
const home = tmpHome();
t.after(() => fs.rmSync(home, { recursive: true, force: true }));

const hive = new HiveManager(() => home);
const errors = [];
t.mock.method(console, 'error', (...args) => errors.push(args));
hive.installPiHooks = () => { throw new Error('synthetic bridge failure'); };

const injection = await hive.ensureAgent({
id: 'pi-degraded-test',
name: 'Meredith',
provider: 'pi',
cwd: home
});

assert.equal(injection.env.PI_CODING_AGENT_DIR, undefined);
assert.equal(injection.args.length, 1);
assert.match(injection.args[0], /^You are "Meredith" \(pi-degraded-test\),/);
assert.match(injection.args[0], /HIVE PROTOCOL/);
assert.equal(errors.length, 1);
assert.match(String(errors[0][0]), /install hooks bridge failed/);
});
32 changes: 32 additions & 0 deletions test/provider-config.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const assert = require('node:assert/strict');
const loadTs = require('./load-ts.cjs');

const {
AGENT_PROVIDER_PRESETS,
inferAgentProvider,
isAgentProvider,
providerPreset
Expand Down Expand Up @@ -58,6 +59,37 @@ test('Grok is a first-class inferred provider with hooks, resume, and always-app
assert.equal(preset.resumeFlag, '--resume');
});

test('Pi is a first-class inferred provider with hooks, positional bootstrap, and resume', () => {
assert.equal(isAgentProvider('pi'), true);
assert.equal(inferAgentProvider('pi --model anthropic/claude-sonnet-4-5'), 'pi');

const preset = providerPreset('pi');
assert.equal(preset.defaultCommand, 'pi');
assert.equal(preset.canReceiveInbox, true);
assert.deepEqual(preset.bridge, { kind: 'hooks', shim: 'pi' });
assert.equal(preset.initialPromptFlag, undefined);
assert.equal(preset.positionalInitialPrompt, true);
assert.equal(preset.resumeFlag, '--session');
});

test('every non-hive-aware inbox provider declares exactly one bootstrap delivery path', () => {
for (const preset of AGENT_PROVIDER_PRESETS) {
if (preset.hiveAware || !preset.canReceiveInbox) continue;

const deliveries = [
typeof preset.initialPromptFlag === 'string' && preset.initialPromptFlag.length > 0,
preset.positionalInitialPrompt === true,
preset.seedDelivery !== undefined
].filter(Boolean).length;

assert.equal(
deliveries,
1,
`${preset.id} can receive hive inbox but declares ${deliveries} bootstrap delivery paths`
);
}
});

test('provider commands use matching models and equivalent bypass modes', () => {
assert.equal(
buildSpawnCommand(autoConfig, 'claude-sonnet-5', 'claude'),
Expand Down
Loading