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
18 changes: 18 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Default routing for typed commands: prefer the machine brain endpoint if reachab
| `$ADE_HOME/sock/ade.sock` | ADE brain local endpoint (POSIX). |
| `\\.\pipe\ade-runtime` | ADE runtime named-pipe endpoint (Windows). |
| `$ADE_HOME/projects.json` | Project catalog. |
| `$ADE_HOME/personal-chats/` | Machine-owned projectless chat runtime state, hidden workspace, transcripts, and attachments. |
| `~/.ade/secrets/` | Machine credential store (`credentials.safe.enc` for desktop safeStorage, `credentials.json.enc` plus `.machine-key` for headless fallback storage, and per-store `*.lock` files). |
| `~/.ade/bin/ade` | Bundled static runtime binary (release installs / remote uploads). |
| `~/.ade/agent-skills/` | Bundled, version-locked ADE agent skills. Desktop remote bootstrap uploads this beside the remote runtime; CLI launch then re-seeds ADE-managed skills into runtime-native home skill directories. |
Expand Down Expand Up @@ -182,6 +183,7 @@ The runtime exposes two layers of JSON-RPC methods (`src/multiProjectRpcServer.t
ade/initialize ade/initialized ping shutdown exit
runtime/info machineInfo.get
projects.list projects.add projects.remove projects.touch
personalChats.call personalChats.streamEvents
runtimeEvents.subscribe runtimeEvents.unsubscribe
sync.getStatus sync.refreshDiscovery
sync.listDevices sync.updateLocalDevice
Expand All @@ -196,6 +198,17 @@ sync.getRequireDpop sync.setRequireDpop

`runtimeEvents.subscribe` returns `eventEpoch`, `nextCursor`, `hasMore`, `gap`, and `oldestCursor`; when `gap` is true, the caller's cursor predates the retained buffer and it should refresh state before resuming from `oldestCursor` / `nextCursor`.

`personalChats.call` dispatches the machine action registry advertised as
`capabilities.personalChats` during initialization. It owns chats outside every
project and includes lifecycle, model, input/approval, attachment, and personal
terminal actions. Typed CLI commands use `ade chat … --personal`; use
`ade chat actions --personal` and `ade chat action --personal <action>
--input-json '{...}'` for the complete low-level registry. These commands require
the machine brain (which can run headlessly without desktop UI) and also work
through the `ade rpc --stdio` transport used by remote desktops. The one-shot
global `--headless` mode is not supported because its in-process runtime exits
with the command.

**Project-scoped** — every other request must carry `params.projectId`. `ade/actions/call` (and the legacy ADE action / tool catalog underneath it) is dispatched into the per-project `ProjectScope` returned by `ProjectScopeRegistry.get(projectId)`.

`ade/initialize` advertises `runtimeInfo.multiProject: true` and `capabilities.projects: true`. Clients use that to switch between sending `projectId` per request (multi-project runtime) and the legacy per-process binding (embedded runtime). Sync is owned by the sync service for the most-recently-opened registered project; `ProjectScopeRegistry.ensureSyncHost` refreshes the active sync project when projects are added or removed.
Expand Down Expand Up @@ -266,6 +279,11 @@ ade lanes create-from-linear --issue-id ENG-431 --start-chat --provider codex --
ade lanes batch-create-from-linear --linear-issues-json '[{"id":"...","identifier":"ENG-431"},{"id":"...","identifier":"ENG-440"}]'
ade chat attach-linear-issue <session> --issue-id ENG-431
ade chat create --from-linear-issue ENG-431
ade chat list --personal --text
ade chat create --personal --provider codex --model openai/gpt-5.5 --prompt "Plan a trip"
ade chat steer personal-session-id --personal --text "focus on the tradeoffs"
ade chat actions --personal --text
ade chat action --personal modelCatalog --input-json '{"mode":"cached"}' --json
ade linear attach --this-session --issue-id ENG-431 # attach to the current CLI session ($ADE_CHAT_SESSION_ID)
ade linear comment "Pushed a fix; CI running" # write back through the attached runtime
ade linear set-state ENG-431 <state-id>
Expand Down
87 changes: 51 additions & 36 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export type AdeRuntimeSyncOptions = {
projectCatalogProvider?: Parameters<typeof createSyncService>[0]["projectCatalogProvider"];
rosterProvider?: Parameters<typeof createSyncService>[0]["rosterProvider"];
foreignChatProvider?: Parameters<typeof createSyncService>[0]["foreignChatProvider"];
personalChatScope?: Parameters<typeof createSyncService>[0]["personalChatScope"];
remoteCommandExecutor?: Parameters<typeof createSyncService>[0]["remoteCommandExecutor"];
/**
* Brain-level websocket listener shared by every project scope's sync host
Expand Down Expand Up @@ -420,22 +421,30 @@ function createHeadlessAdeCliAgentEnv(baseEnv: NodeJS.ProcessEnv = process.env):
export async function createAdeRuntime(args: {
projectRoot: string;
workspaceRoot?: string;
primaryWorktreePath?: string;
chatRuntime?: "headless-stub" | "agent";
runtimeProfile?: "full" | "chat";
/** Disable project-oriented push/deep-link events for machine-scoped runtimes. */
publishPushEvents?: boolean;
syncRuntime?: AdeRuntimeSyncOptions;
} | string): Promise<AdeRuntime> {
const resolvedArgs = typeof args === "string"
? { projectRoot: args, workspaceRoot: args }
: args;
const projectRoot = path.resolve(resolvedArgs.projectRoot);
const workspaceRoot = path.resolve(resolvedArgs.workspaceRoot ?? resolvedArgs.projectRoot);
const primaryWorktreePath = path.resolve(resolvedArgs.primaryWorktreePath ?? resolvedArgs.projectRoot);
const chatOnlyRuntime = resolvedArgs.runtimeProfile === "chat";
const publishPushEvents = resolvedArgs.publishPushEvents !== false;
if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
throw new Error(`Project root does not exist: ${projectRoot}`);
}
if (!fs.existsSync(workspaceRoot) || !fs.statSync(workspaceRoot).isDirectory()) {
throw new Error(`Workspace root does not exist: ${workspaceRoot}`);
}
if (!fs.existsSync(primaryWorktreePath) || !fs.statSync(primaryWorktreePath).isDirectory()) {
throw new Error(`Primary worktree path does not exist: ${primaryWorktreePath}`);
}

const hadAdeDb = fs.existsSync(path.join(projectRoot, ".ade", "ade.db"));
const baseRef = await detectDefaultBaseRef(projectRoot);
Expand Down Expand Up @@ -492,6 +501,7 @@ export async function createAdeRuntime(args: {
const laneService = createLaneService({
db,
projectRoot,
primaryWorktreePath,
projectId,
defaultBaseRef: baseRef,
worktreesDir: paths.worktreesDir,
Expand Down Expand Up @@ -1238,42 +1248,46 @@ export async function createAdeRuntime(args: {
machineName: os.hostname(),
};
});
const detachPushSources = pushPublisherService.attachSources(projectId, {
agentChatService: agentChatService ?? null,
ptyService,
subscribePrNotifications: (cb) => {
pushPrNotificationSubscribers.add(cb);
return () => pushPrNotificationSubscribers.delete(cb);
},
resolveLaneName: (laneId) => {
try {
const row = db.get<{ name: string }>(
"select name from lanes where id = ? and project_id = ? limit 1",
[laneId, projectId],
);
return row?.name ?? null;
} catch {
return null;
}
},
resolveCliSession: (sessionId) => {
try {
const session = sessionService.get(sessionId);
if (!session) return null;
return {
title: session.title ?? null,
toolType: session.toolType ?? null,
chatSessionId: session.chatSessionId ?? null,
};
} catch {
return null;
}
},
});
pushPublisherForPtySignals = pushPublisherService;
void pushPublisherService.start().catch((error) => {
logger.warn("push.start_failed", { error: error instanceof Error ? error.message : String(error) });
});
const detachPushSources = publishPushEvents
? pushPublisherService.attachSources(projectId, {
agentChatService: agentChatService ?? null,
ptyService,
subscribePrNotifications: (cb) => {
pushPrNotificationSubscribers.add(cb);
return () => pushPrNotificationSubscribers.delete(cb);
},
resolveLaneName: (laneId) => {
try {
const row = db.get<{ name: string }>(
"select name from lanes where id = ? and project_id = ? limit 1",
[laneId, projectId],
);
return row?.name ?? null;
} catch {
return null;
}
},
resolveCliSession: (sessionId) => {
try {
const session = sessionService.get(sessionId);
if (!session) return null;
return {
title: session.title ?? null,
toolType: session.toolType ?? null,
chatSessionId: session.chatSessionId ?? null,
};
} catch {
return null;
}
},
})
: () => {};
if (publishPushEvents) {
pushPublisherForPtySignals = pushPublisherService;
void pushPublisherService.start().catch((error) => {
logger.warn("push.start_failed", { error: error instanceof Error ? error.message : String(error) });
});
}

const usageTrackingService = createUsageTrackingService({
logger,
Expand Down Expand Up @@ -1374,6 +1388,7 @@ export async function createAdeRuntime(args: {
projectCatalogProvider: resolvedArgs.syncRuntime.projectCatalogProvider,
rosterProvider: resolvedArgs.syncRuntime.rosterProvider,
foreignChatProvider: resolvedArgs.syncRuntime.foreignChatProvider,
personalChatScope: resolvedArgs.syncRuntime.personalChatScope,
remoteCommandExecutor: resolvedArgs.syncRuntime.remoteCommandExecutor,
getModelPickerStore: () => getSharedModelPickerStore(db),
cloudRelayStore,
Expand Down
Loading