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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ jobs:
ci-pass:
if: always()
needs:
- install
- secret-scan
- typecheck-desktop
- typecheck-ade-cli
Expand All @@ -209,8 +210,9 @@ jobs:
- name: Check all jobs passed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]] ||
[[ "${{ contains(needs.*.result, 'skipped') }}" == "true" ]] ||
[[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "::error::One or more required jobs failed or were cancelled"
echo "::error::One or more required jobs failed, were skipped, or were cancelled"
exit 1
fi
echo "All CI jobs passed"
220 changes: 9 additions & 211 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import {
} from "../../desktop/src/main/services/computerUse/localComputerUse";
import { loadAgentBrowserArtifactPayloadFromFile, parseAgentBrowserArtifactPayload } from "../../desktop/src/main/services/proof/agentBrowserArtifactAdapter";
import { resolveAgentMemoryWritePolicy } from "../../desktop/src/main/services/memory/memoryService";
import {
ADE_ACTION_ALLOWLIST,
type AdeActionDomain,
getAdeActionDomainServices,
isAllowedAdeAction,
listAllowedAdeActionNames,
} from "../../desktop/src/main/services/adeActions/registry";
import { ReflectionValidationError } from "../../desktop/src/main/services/orchestrator/orchestratorService";
import { getTeamMembersForRun, registerTeamMember, updateTeamMemberStatus } from "../../desktop/src/main/services/orchestrator/teamRuntimeState";
import { launchPrIssueResolutionChat, previewPrIssueResolutionPrompt } from "../../desktop/src/main/services/prs/prIssueResolver";
Expand Down Expand Up @@ -232,6 +239,8 @@ const TOOL_SPECS: ToolSpec[] = [
"process",
"pty",
"computer_use_artifacts",
"automations",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟡 Medium] [🔵 Bug]

run_ade_action now exposes two new domains:

// apps/ade-cli/src/adeRpcServer.ts
"computer_use_artifacts",
"automations",
"issue",
],

but the headless runtime built by createAdeRuntime() in @apps/ade-cli/src/bootstrap.ts still returns no githubService, automationService, or automationPlannerService. I verified that getAdeActionDomainServices(runtime) depends on those optional runtime fields in @apps/desktop/src/main/services/adeActions/registry.ts, so in headless mode both new domains resolve to null and run_ade_action fails with Domain 'automations'/'issue' is unavailable in this runtime. This makes the new public RPC surface unusable for headless callers. Either construct those services in the headless bootstrap path or do not advertise these domains there until they exist.

"issue",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 High] [🔵 Bug]

The new issue domain is also advertised in run_ade_action, but headless CLI cannot service it. buildIssueDomainService() in @apps/desktop/src/main/services/adeActions/registry.ts requires runtime.githubService plus the new issue mutation helpers (addIssueComment, setIssueLabels, closeIssue, etc.). Headless runtime creation never assigns runtime.githubService, and the internal HeadlessGitHubService type in @apps/ade-cli/src/headlessLinearServices.ts only exposes getStatus, getRepoOrThrow, getTokenOrThrow, and apiRequest. As a result, the domain is unavailable or incompatible whenever the CLI runs without a desktop socket. Fix by exporting a compatible GitHub service from headless services and threading it into createAdeRuntime(), or by suppressing the issue domain in headless mode.

// apps/ade-cli/src/adeRpcServer.ts
            "computer_use_artifacts",
            "automations",
            "issue",

],
},
action: { type: "string", minLength: 1 },
Expand Down Expand Up @@ -2992,217 +3001,6 @@ async function waitForTestRunCompletion(args: {
};
}

type AdeActionDomain =
| "lane"
| "git"
| "diff"
| "conflicts"
| "pr"
| "tests"
| "chat"
| "mission"
| "orchestrator"
| "orchestrator_core"
| "memory"
| "cto_state"
| "worker_agent"
| "session"
| "operation"
| "project_config"
| "issue_inventory"
| "flow_policy"
| "linear_dispatcher"
| "linear_issue_tracker"
| "linear_sync"
| "linear_ingress"
| "linear_routing"
| "file"
| "process"
| "pty"
| "computer_use_artifacts";

const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly string[]>> = {
lane: [
"adoptAttached",
"attach",
"create",
"createFromUnstaged",
"delete",
"getChildren",
"getStackChain",
"importBranch",
"list",
"listUnregisteredWorktrees",
"refreshSnapshots",
"rename",
"reparent",
"updateAppearance",
],
git: [
"abortRebase",
"cherryPickCommit",
"commit",
"continueRebase",
"fetch",
"getCommitMessage",
"getConflictState",
"getFileHistory",
"getSyncStatus",
"listCommitFiles",
"mergeAbort",
"mergeContinue",
"pull",
"push",
"rebaseAbort",
"rebaseContinue",
"revertCommit",
"stash",
"stagePaths",
"unstagePaths",
],
diff: ["getChanges", "getFileDiff"],
conflicts: ["getLaneStatus", "listOverlaps", "rebaseLane", "runPrediction"],
pr: [
"addComment",
"aiReviewSummary",
"cleanupIntegrationWorkflow",
"createFromLane",
"createIntegrationLane",
"createIntegrationPr",
"createQueuePrs",
"dismissIntegrationCleanup",
"draftDescription",
"getActionRuns",
"getChecks",
"getComments",
"getDetail",
"getGithubSnapshot",
"getIntegrationResolutionState",
"getMobileSnapshot",
"getPrHealth",
"getQueueState",
"getReviewThreads",
"getReviews",
"landQueueNext",
"landStack",
"landStackEnhanced",
"linkToLane",
"listAll",
"listGroupPrs",
"listIntegrationProposals",
"listIntegrationWorkflows",
"listWithConflicts",
"postReviewComment",
"reactToComment",
"recheckIntegrationStep",
"refresh",
"reorderQueuePrs",
"requestReviewers",
"setLabels",
"setReviewThreadResolved",
"simulateIntegration",
"startIntegrationResolution",
"submitReview",
"updateDescription",
"updateIntegrationProposal",
"updateTitle",
],
tests: ["getLogTail", "listRuns", "listSuites", "run", "stop"],
chat: [
"createSession",
"deleteSession",
"getAvailableModels",
"getSessionSummary",
"getSlashCommands",
"interrupt",
"listSessions",
"resumeSession",
"sendMessage",
],
memory: ["addSharedFact", "pinMemory", "searchMemories", "writeMemory"],
session: ["get", "readTranscriptTail"],
operation: ["finish", "list", "start"],
project_config: ["get", "save"],
issue_inventory: [
"getConvergenceRuntime",
"getConvergenceStatus",
"getInventory",
"getNewItems",
"getPipelineSettings",
"markDismissed",
"markEscalated",
"markFixed",
"markSentToAgent",
"reconcileConvergenceSessionExit",
"savePipelineSettings",
"syncFromPrData",
],
flow_policy: ["getPolicy", "savePolicy"],
linear_dispatcher: ["dispatchIssue", "getDashboard", "listEmployees", "listQueue"],
linear_issue_tracker: ["getStatus", "listIssues"],
linear_sync: ["getDashboard", "getRunDetail", "listQueue", "resolveQueueItem", "runSyncNow"],
linear_ingress: ["ensureRelayWebhook", "getStatus", "listRecentEvents"],
linear_routing: ["simulateRoute"],
file: [
"createDirectory",
"createFile",
"deletePath",
"listTree",
"listWorkspaces",
"quickOpen",
"readFile",
"rename",
"searchText",
"writeWorkspaceText",
],
process: ["getLogTail", "listDefinitions", "listRuntime", "startAll", "stopAll"],
pty: ["create", "dispose", "resize", "write"],
computer_use_artifacts: ["ingest", "listArtifacts"],
};

function getAdeActionDomainServices(runtime: AdeRuntime): Partial<Record<AdeActionDomain, Record<string, unknown> | null | undefined>> {
return {
lane: runtime.laneService as unknown as Record<string, unknown>,
git: runtime.gitService as unknown as Record<string, unknown>,
diff: runtime.diffService as unknown as Record<string, unknown>,
conflicts: runtime.conflictService as unknown as Record<string, unknown>,
pr: (runtime.prService ?? null) as unknown as Record<string, unknown> | null,
tests: runtime.testService as unknown as Record<string, unknown>,
chat: (runtime.agentChatService ?? null) as unknown as Record<string, unknown> | null,
mission: runtime.missionService as unknown as Record<string, unknown>,
orchestrator: runtime.aiOrchestratorService as unknown as Record<string, unknown>,
orchestrator_core: runtime.orchestratorService as unknown as Record<string, unknown>,
memory: runtime.memoryService as unknown as Record<string, unknown>,
cto_state: runtime.ctoStateService as unknown as Record<string, unknown>,
worker_agent: runtime.workerAgentService as unknown as Record<string, unknown>,
session: runtime.sessionService as unknown as Record<string, unknown>,
operation: runtime.operationService as unknown as Record<string, unknown>,
project_config: runtime.projectConfigService as unknown as Record<string, unknown>,
issue_inventory: runtime.issueInventoryService as unknown as Record<string, unknown>,
flow_policy: (runtime.flowPolicyService ?? null) as unknown as Record<string, unknown> | null,
linear_dispatcher: (runtime.linearDispatcherService ?? null) as unknown as Record<string, unknown> | null,
linear_issue_tracker: (runtime.linearIssueTracker ?? null) as unknown as Record<string, unknown> | null,
linear_sync: (runtime.linearSyncService ?? null) as unknown as Record<string, unknown> | null,
linear_ingress: (runtime.linearIngressService ?? null) as unknown as Record<string, unknown> | null,
linear_routing: (runtime.linearRoutingService ?? null) as unknown as Record<string, unknown> | null,
file: (runtime.fileService ?? null) as unknown as Record<string, unknown> | null,
process: (runtime.processService ?? null) as unknown as Record<string, unknown> | null,
pty: runtime.ptyService as unknown as Record<string, unknown>,
computer_use_artifacts: runtime.computerUseArtifactBrokerService as unknown as Record<string, unknown>,
};
}

function listAllowedAdeActionNames(domain: AdeActionDomain, service: Record<string, unknown>): string[] {
const allowed = ADE_ACTION_ALLOWLIST[domain] ?? [];
return allowed
.filter((key) => typeof service[key] === "function")
.sort((a, b) => a.localeCompare(b));
}

function isAllowedAdeAction(domain: AdeActionDomain, action: string): boolean {
return (ADE_ACTION_ALLOWLIST[domain] ?? []).includes(action);
}

async function waitForSessionCompletion(args: {
runtime: AdeRuntime;
ptyId: string;
Expand Down
65 changes: 63 additions & 2 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ import {
} from "../../desktop/src/main/services/computerUse/computerUseArtifactBrokerService";
import type { createFileService } from "../../desktop/src/main/services/files/fileService";
import type { createProcessService } from "../../desktop/src/main/services/processes/processService";
import type { createGithubService } from "../../desktop/src/main/services/github/githubService";
import {
createAutomationService,
type AutomationAdeActionRegistry,
} from "../../desktop/src/main/services/automations/automationService";
import { createAutomationPlannerService } from "../../desktop/src/main/services/automations/automationPlannerService";
import {
ADE_ACTION_ALLOWLIST,
type AdeActionDomain,
getAdeActionDomainServices,
isAllowedAdeAction,
} from "../../desktop/src/main/services/adeActions/registry";
import { createHeadlessLinearServices } from "./headlessLinearServices";
import { createEventBuffer, type BufferedEvent, type EventBuffer } from "./eventBuffer";

Expand Down Expand Up @@ -93,6 +105,9 @@ export type AdeRuntime = {
linearIngressService?: ReturnType<typeof createLinearIngressService> | null;
linearRoutingService?: ReturnType<typeof createLinearRoutingService> | null;
processService?: ReturnType<typeof createProcessService> | null;
githubService?: ReturnType<typeof createGithubService> | null;
automationService?: ReturnType<typeof createAutomationService> | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 High] [🔵 Bug]

The diff extends AdeRuntime with automation fields, but createAdeRuntime() still never constructs or returns them in the headless path. The new CLI subcommands all dispatch through run_ade_action, and the handler rejects missing domains, so ade --headless automations ... (or any invocation when the desktop socket is unavailable) fails with Domain 'automations' is unavailable in this runtime. instead of running the requested action.

// apps/ade-cli/src/bootstrap.ts
githubService?: ReturnType<typeof createGithubService> | null;
automationService?: ReturnType<typeof createAutomationService> | null;
automationPlannerService?: ReturnType<typeof createAutomationPlannerService> | null;

I verified the execution path in @apps/ade-cli/src/cli.ts (createConnection() -> createAdeRuntime()), @apps/ade-cli/src/adeRpcServer.ts (run_ade_action), and @apps/desktop/src/main/services/adeActions/registry.ts (buildAutomationsDomainService). Fix by actually instantiating and returning the GitHub/automation services for headless mode, or by explicitly making the new commands unavailable there.

automationPlannerService?: ReturnType<typeof createAutomationPlannerService> | null;
computerUseArtifactBrokerService: ComputerUseArtifactBrokerService;
orchestratorService: ReturnType<typeof createOrchestratorService>;
aiOrchestratorService: ReturnType<typeof createAiOrchestratorService>;
Expand Down Expand Up @@ -361,7 +376,30 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
openExternal: async () => {},
});

return {
const agentChatService = headlessLinearServices.agentChatService as unknown as ReturnType<typeof createAgentChatService> | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟡 Medium] [🔵 Bug]

This new headless bootstrap path casts headlessLinearServices.agentChatService to the full desktop createAgentChatService type and passes it into createAutomationService, but the headless implementation does not implement runSessionTurn. automationService calls agentChatServiceRef.runSessionTurn(...) for both built-in agent-session actions and execution.kind === "agent-session", so ade automations run <id> now fails for any session-based rule in headless CLI mode with a runtime ...runSessionTurn is not a function error instead of executing the automation. ts // apps/ade-cli/src/bootstrap.ts const agentChatService = headlessLinearServices.agentChatService as unknown as ReturnType<typeof createAgentChatService> | null; const automationService = createAutomationService({ db, logger, projectId, Fix by either implementing the missing runSessionTurn contract on the headless chat service or by not passing/advertising an agent chat service to automations until headless mode can actually execute session turns.

const automationService = createAutomationService({
db,
logger,
projectId,
projectRoot,
laneService,
projectConfigService,
conflictService,
testService,
agentChatService: agentChatService ?? undefined,
missionService,
aiOrchestratorService,
onEvent: (event) => pushEvent("runtime", { ...event, source: "automations" }),
});
const automationPlannerService = createAutomationPlannerService({
logger,
projectRoot,
projectConfigService,
laneService,
automationService,
});

const runtime: AdeRuntime = {
projectRoot,
workspaceRoot,
projectId,
Expand All @@ -379,11 +417,12 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
missionService,
ptyService,
testService,
agentChatService: headlessLinearServices.agentChatService as unknown as ReturnType<typeof createAgentChatService> | null,
agentChatService,
issueInventoryService,
memoryService,
ctoStateService,
workerAgentService,
githubService: headlessLinearServices.githubService as never,
prService: headlessLinearServices.prService,
fileService: headlessLinearServices.fileService,
flowPolicyService: headlessLinearServices.flowPolicyService,
Expand All @@ -393,12 +432,15 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
linearIngressService: headlessLinearServices.linearIngressService,
linearRoutingService: headlessLinearServices.linearRoutingService,
processService: headlessLinearServices.processService,
automationService,
automationPlannerService,
computerUseArtifactBrokerService,
orchestratorService,
aiOrchestratorService,
eventBuffer,
dispose: () => {
const swallow = (fn: () => void) => { try { fn(); } catch { /* ignore */ } };
swallow(() => automationService.dispose());
swallow(() => headlessLinearServices.dispose());
swallow(() => aiOrchestratorService.dispose());
swallow(() => testService.disposeAll());
Expand All @@ -407,4 +449,23 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo
swallow(() => db.close());
}
};

const adeActionLookup: AutomationAdeActionRegistry = {
isAllowed(domain: string, action: string): boolean {
return isAllowedAdeAction(domain as AdeActionDomain, action);
},
getService(domain: string): Record<string, unknown> | null {
const services = getAdeActionDomainServices(runtime);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟡 Medium] [🔵 Bug]

@apps/ade-cli/src/bootstrap.ts binds the new automation ADE-action registry against getAdeActionDomainServices(runtime), but the runtime object built just above never populates githubService before the registry is used:

// apps/ade-cli/src/bootstrap.ts
getService(domain: string): Record<string, unknown> | null {
  const services = getAdeActionDomainServices(runtime);
  return (services[domain as AdeActionDomain] ?? null) as Record<string, unknown> | null;
}

In this PR the shared ADE-action registry adds the new issue domain, and buildIssueDomainService() returns null unless runtime.githubService is present. The desktop app explicitly includes githubService in its ADE-action runtime bag, but this headless CLI runtime does not, so ade automations run <id> will fail for any rule whose adeAction.domain is issue with Service for domain 'issue' is not available in this process. even though the planner/test suite now accepts type: "ade-action" rules like domain: "issue", action: "setLabels". Expose a GitHub service on the headless runtime with the same repo-detection and issue-mutation helpers as the desktop githubService, or stop advertising the issue ADE-action domain in headless mode.

return (services[domain as AdeActionDomain] ?? null) as Record<string, unknown> | null;
},
listDomains(): string[] {
return Object.keys(ADE_ACTION_ALLOWLIST);
},
listActions(domain: string): string[] {
return [...(ADE_ACTION_ALLOWLIST[domain as AdeActionDomain] ?? [])];
},
};
automationService.bindAdeActionRegistry(adeActionLookup);

return runtime;
}
Loading
Loading