Skip to content

Commit 5bb8c02

Browse files
authored
Merge pull request #106 from arul28/onboarding-fixes
Onboarding fixes
2 parents 5535c98 + 070c288 commit 5bb8c02

48 files changed

Lines changed: 3614 additions & 757 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ade/ade.yaml

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,6 @@
11
version: 1
2-
processes:
3-
- id: ad55deza
4-
name: Dev
5-
command:
6-
- npm
7-
- run
8-
- dev
9-
cwd: apps/desktop
2+
processes: []
103
stackButtons: []
114
testSuites: []
125
laneOverlayPolicies: []
136
automations: []
14-
ai:
15-
features:
16-
narratives: true
17-
conflict_proposals: true
18-
commit_messages: true
19-
pr_descriptions: true
20-
terminal_summaries: true
21-
memory_consolidation: true
22-
mission_planning: true
23-
orchestrator: true
24-
initial_context: true
25-
featureModelOverrides:
26-
commit_messages: openai/gpt-5.3-codex-spark
27-
pr_descriptions: openai/gpt-5.3-codex-spark
28-
terminal_summaries: openai/gpt-5.3-codex-spark
29-
chat:
30-
autoTitleEnabled: true
31-
autoTitleModelId: openai/gpt-5.3-codex-spark
32-
autoTitleRefreshOnComplete: true

.ade/cto/identity.yaml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
name: CTO
2-
version: 3
3-
persona: Persistent project CTO with strategic personality.
2+
version: 1
3+
persona: >-
4+
You are the CTO for this project inside ADE.
5+
6+
You are the persistent technical lead who owns architecture, execution
7+
quality, engineering continuity, and team direction.
8+
9+
Use ADE's tools and project context to help the team move forward with clear,
10+
concrete decisions.
411
personality: strategic
512
modelPreferences:
613
provider: claude
@@ -21,8 +28,4 @@ openclawContextPolicy:
2128
- secret
2229
- token
2330
- system_prompt
24-
onboardingState:
25-
completedSteps:
26-
- identity
27-
completedAt: 2026-03-26T18:45:21.214Z
28-
updatedAt: 2026-03-26T18:45:21.216Z
31+
updatedAt: 1970-01-01T00:00:00.000Z

apps/desktop/src/main/main.ts

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { app, BrowserWindow, nativeImage, shell } from "electron";
2-
import { execFileSync } from "node:child_process";
32
import path from "node:path";
43
type NodePtyType = typeof import("node-pty");
54
import { registerIpc } from "./services/ipc/registerIpc";
@@ -29,6 +28,7 @@ import { createGitOperationsService } from "./services/git/gitOperationsService"
2928
import { runGit } from "./services/git/git";
3029
import { createJobEngine } from "./services/jobs/jobEngine";
3130
import { createAiIntegrationService } from "./services/ai/aiIntegrationService";
31+
import { augmentProcessPathWithShellAndKnownCliDirs } from "./services/ai/cliExecutableResolver";
3232
import { createAgentChatService } from "./services/chat/agentChatService";
3333
import { createGithubService } from "./services/github/githubService";
3434
import { createPrService } from "./services/prs/prService";
@@ -113,38 +113,13 @@ import type { Logger } from "./services/logging/logger";
113113
* the AI SDK can locate the CLI.
114114
*/
115115
function fixElectronShellPath(): void {
116-
if (process.platform !== "darwin" && process.platform !== "linux") return;
117-
118-
const currentPath = process.env.PATH ?? "";
119-
const hasUserLocalBin = currentPath.includes(".local/bin");
120-
const hasCommonCliBin = currentPath.includes("/usr/local/bin") || currentPath.includes("/opt/homebrew/bin");
121-
// Already rich — likely launched from terminal or already fixed.
122-
if (hasUserLocalBin && hasCommonCliBin) return;
123-
124-
try {
125-
const loginShell = process.env.SHELL || "/bin/zsh";
126-
// Use execFileSync so SHELL is treated as a path, not interpolated shell text.
127-
const resolved = execFileSync(loginShell, ["-lc", 'printf "%s" "$PATH"'], {
128-
encoding: "utf-8",
129-
timeout: 5_000,
130-
}).trim();
131-
132-
if (resolved && resolved.length > currentPath.length) {
133-
process.env.PATH = resolved;
134-
}
135-
} catch {
136-
// Shell resolution failed — manually append common paths as fallback.
137-
const extras = [
138-
"/usr/local/bin",
139-
"/opt/homebrew/bin",
140-
"/opt/homebrew/sbin",
141-
`${process.env.HOME}/.local/bin`,
142-
`${process.env.HOME}/.nvm/current/bin`,
143-
].filter((p) => !currentPath.includes(p));
144-
145-
if (extras.length) {
146-
process.env.PATH = `${currentPath}:${extras.join(":")}`;
147-
}
116+
const nextPath = augmentProcessPathWithShellAndKnownCliDirs({
117+
env: process.env,
118+
includeInteractiveShell: true,
119+
timeoutMs: 1_500,
120+
});
121+
if (nextPath) {
122+
process.env.PATH = nextPath;
148123
}
149124
}
150125

apps/desktop/src/main/services/ai/aiIntegrationService.test.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ beforeEach(() => {
160160

161161
describe("aiIntegrationService", () => {
162162
it("routes executeTask through unified executor", async () => {
163-
const { service, runCalls } = makeService();
163+
const { service, runCalls } = makeService({
164+
aiConfig: { features: { mission_planning: true } },
165+
});
164166

165167
const result = await service.executeTask({
166168
feature: "mission_planning",
@@ -176,18 +178,25 @@ describe("aiIntegrationService", () => {
176178
expect(usageInsertCalls(runCalls)).toHaveLength(1);
177179
});
178180

179-
it("treats commit_messages as opt-in until explicitly enabled", () => {
181+
it("preserves legacy defaults for missing AI feature toggles", () => {
180182
const { service } = makeService();
181183
const { service: enabledService } = makeService({
182184
aiConfig: {
183185
features: {
184186
commit_messages: true,
187+
terminal_summaries: true,
188+
pr_descriptions: true,
185189
},
186190
},
187191
});
188192

189193
expect(service.getFeatureFlag("commit_messages")).toBe(false);
194+
expect(service.getFeatureFlag("terminal_summaries")).toBe(true);
195+
expect(service.getFeatureFlag("pr_descriptions")).toBe(true);
196+
expect(service.getFeatureFlag("orchestrator")).toBe(true);
190197
expect(enabledService.getFeatureFlag("commit_messages")).toBe(true);
198+
expect(enabledService.getFeatureFlag("terminal_summaries")).toBe(true);
199+
expect(enabledService.getFeatureFlag("pr_descriptions")).toBe(true);
191200
});
192201

193202
it("routes generated commit messages through the commit_messages feature", async () => {
@@ -248,7 +257,9 @@ describe("aiIntegrationService", () => {
248257
});
249258

250259
it("uses planning tools for mission planning tasks", async () => {
251-
const { service } = makeService();
260+
const { service } = makeService({
261+
aiConfig: { features: { mission_planning: true } },
262+
});
252263

253264
await service.executeTask({
254265
feature: "mission_planning",
@@ -264,7 +275,9 @@ describe("aiIntegrationService", () => {
264275
});
265276

266277
it("resolves a default task model when model is omitted", async () => {
267-
const { service } = makeService();
278+
const { service } = makeService({
279+
aiConfig: { features: { orchestrator: true } },
280+
});
268281

269282
await service.executeTask({
270283
feature: "orchestrator",
@@ -280,7 +293,9 @@ describe("aiIntegrationService", () => {
280293
});
281294

282295
it("resolves a default model for memory consolidation tasks when model is omitted", async () => {
283-
const { service } = makeService();
296+
const { service } = makeService({
297+
aiConfig: { features: { memory_consolidation: true } },
298+
});
284299

285300
await service.executeTask({
286301
feature: "memory_consolidation",
@@ -296,7 +311,9 @@ describe("aiIntegrationService", () => {
296311
});
297312

298313
it("uses planning tools for read-only orchestrator tasks and none for other read-only tasks", async () => {
299-
const { service } = makeService();
314+
const { service } = makeService({
315+
aiConfig: { features: { orchestrator: true, terminal_summaries: true } },
316+
});
300317

301318
await service.executeTask({
302319
feature: "orchestrator",
@@ -324,7 +341,9 @@ describe("aiIntegrationService", () => {
324341
});
325342

326343
it("forwards memory context and compaction identifiers to the unified executor when provided", async () => {
327-
const { service } = makeService();
344+
const { service } = makeService({
345+
aiConfig: { features: { orchestrator: true } },
346+
});
328347
const memoryService = {
329348
writeMemory: vi.fn(),
330349
} as any;

apps/desktop/src/main/services/ai/aiIntegrationService.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ type RuntimeTaskDefaults = {
121121
timeoutMs: number;
122122
};
123123

124+
const DEFAULT_AI_FEATURE_FLAGS: Record<AiFeatureKey, boolean> = {
125+
narratives: true,
126+
conflict_proposals: true,
127+
commit_messages: false,
128+
pr_descriptions: true,
129+
terminal_summaries: true,
130+
memory_consolidation: true,
131+
mission_planning: true,
132+
orchestrator: true,
133+
initial_context: true,
134+
};
135+
124136
const DEFAULT_CLAUDE_TASK_MODEL_ID = getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-4-6";
125137
const DEFAULT_CODEX_TASK_MODEL_ID = getDefaultModelDescriptor("codex")?.id ?? "openai/gpt-5.4-codex";
126138

@@ -461,10 +473,7 @@ export function createAiIntegrationService(args: {
461473
const aiConfig = extractAiConfig(snapshot);
462474
const features = isRecord(aiConfig.features) ? aiConfig.features : {};
463475
const value = features[feature];
464-
if (value == null) {
465-
return feature === "commit_messages" ? false : true;
466-
}
467-
return Boolean(value);
476+
return value == null ? DEFAULT_AI_FEATURE_FLAGS[feature] : Boolean(value);
468477
};
469478

470479
const getDailyBudgetLimit = (feature: AiFeatureKey): number | null => {

apps/desktop/src/main/services/ai/authDetector.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import { EventEmitter } from "node:events";
3+
import fs from "node:fs";
4+
import os from "node:os";
5+
import path from "node:path";
36

47
const spawnMock = vi.fn();
8+
const execFileSyncMock = vi.fn();
59
const getAllApiKeysMock = vi.fn();
610

711
/** Helper: create a fake ChildProcess that immediately emits close with the given result. */
@@ -36,6 +40,7 @@ vi.mock("node:child_process", async () => {
3640
return {
3741
...actual,
3842
spawn: (...args: unknown[]) => spawnMock(...args),
43+
execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
3944
};
4045
});
4146

@@ -47,9 +52,18 @@ vi.mock("./apiKeyStore", () => ({
4752
let detectAllAuth: typeof import("./authDetector").detectAllAuth;
4853
let detectCliAuthStatuses: typeof import("./authDetector").detectCliAuthStatuses;
4954
let verifyProviderApiKey: typeof import("./authDetector").verifyProviderApiKey;
55+
const originalPlatform = process.platform;
56+
57+
function setPlatform(value: NodeJS.Platform): void {
58+
Object.defineProperty(process, "platform", {
59+
value,
60+
configurable: true,
61+
});
62+
}
5063

5164
beforeEach(async () => {
5265
vi.resetModules();
66+
setPlatform("darwin");
5367
const mod = await import("./authDetector");
5468
detectAllAuth = mod.detectAllAuth;
5569
detectCliAuthStatuses = mod.detectCliAuthStatuses;
@@ -58,17 +72,24 @@ beforeEach(async () => {
5872

5973
describe("authDetector", () => {
6074
const originalEnv = { ...process.env };
75+
let tempHomeDir: string | null = null;
6176

6277
beforeEach(() => {
6378
spawnMock.mockReset();
79+
execFileSyncMock.mockReset();
6480
getAllApiKeysMock.mockReset();
6581
vi.unstubAllGlobals();
6682
process.env = { ...originalEnv };
6783
});
6884

6985
afterEach(() => {
7086
process.env = { ...originalEnv };
87+
setPlatform(originalPlatform);
7188
vi.unstubAllGlobals();
89+
if (tempHomeDir) {
90+
fs.rmSync(tempHomeDir, { recursive: true, force: true });
91+
tempHomeDir = null;
92+
}
7293
});
7394

7495
it("reports installed-but-unauthenticated CLI providers", async () => {
@@ -236,6 +257,89 @@ describe("authDetector", () => {
236257
expect(claude?.authenticated).toBe(true);
237258
});
238259

260+
it("finds codex through an npm-global prefix when PATH lookup fails", async () => {
261+
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-auth-detector-"));
262+
const prefixDir = path.join(tempHomeDir, ".npm-global");
263+
fs.mkdirSync(path.join(prefixDir, "bin"), { recursive: true });
264+
fs.writeFileSync(path.join(tempHomeDir, ".npmrc"), "prefix=~/.npm-global\n", "utf8");
265+
fs.writeFileSync(path.join(prefixDir, "bin", "codex"), "#!/bin/sh\nexit 0\n", "utf8");
266+
fs.chmodSync(path.join(prefixDir, "bin", "codex"), 0o755);
267+
process.env.HOME = tempHomeDir;
268+
process.env.PATH = "/usr/bin:/bin";
269+
270+
spawnMock.mockImplementation((command: string, args: string[] = []) => {
271+
if (args[0] === "--version") {
272+
if (command === "codex") return fakeError();
273+
if (command === path.join(prefixDir, "bin", "codex")) return fakeChild({ status: 0, stdout: "0.105.0\n" });
274+
return fakeError();
275+
}
276+
if (command === "which") {
277+
return fakeChild({ status: 1 });
278+
}
279+
if ((command === "codex" || command.endsWith("/codex")) && args[0] === "login" && args[1] === "status") {
280+
return fakeChild({ status: 0, stdout: "Authenticated as test-user\n" });
281+
}
282+
return fakeChild({ status: 1 });
283+
});
284+
285+
const statuses = await detectCliAuthStatuses();
286+
const codex = statuses.find((entry) => entry.cli === "codex");
287+
288+
expect(codex).toEqual({
289+
cli: "codex",
290+
installed: true,
291+
path: path.join(prefixDir, "bin", "codex"),
292+
authenticated: true,
293+
verified: true,
294+
});
295+
});
296+
297+
it("repairs PATH from the interactive shell during a forced refresh", async () => {
298+
process.env.PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
299+
process.env.SHELL = "/bin/zsh";
300+
301+
execFileSyncMock.mockImplementation((_command: string, args: string[]) => {
302+
if (args[0] === "-lc") {
303+
return "__ADE_PATH_START__/Users/arul/.local/bin:/usr/local/bin:/usr/bin:/bin__ADE_PATH_END__";
304+
}
305+
if (args[0] === "-ic") {
306+
return "shell noise\n__ADE_PATH_START__/Users/arul/.npm-global/bin:/Users/arul/.local/bin:/usr/local/bin:/usr/bin:/bin__ADE_PATH_END__";
307+
}
308+
throw new Error(`unexpected shell args: ${args.join(" ")}`);
309+
});
310+
311+
spawnMock.mockImplementation((command: string, args: string[] = []) => {
312+
if (args[0] === "--version") {
313+
if (command === "codex" && process.env.PATH?.includes("/Users/arul/.npm-global/bin")) {
314+
return fakeChild({ status: 0, stdout: "codex-cli 0.117.0\n" });
315+
}
316+
return fakeError();
317+
}
318+
if (command === "which") {
319+
if (args[0] === "codex" && process.env.PATH?.includes("/Users/arul/.npm-global/bin")) {
320+
return fakeChild({ status: 0, stdout: "/Users/arul/.npm-global/bin/codex\n" });
321+
}
322+
return fakeChild({ status: 1 });
323+
}
324+
if ((command === "codex" || command.endsWith("/codex")) && args[0] === "login" && args[1] === "status") {
325+
return fakeChild({ status: 0, stdout: "Logged in using ChatGPT\n" });
326+
}
327+
return fakeChild({ status: 1 });
328+
});
329+
330+
const statuses = await detectCliAuthStatuses({ force: true });
331+
const codex = statuses.find((entry) => entry.cli === "codex");
332+
333+
expect(process.env.PATH).toContain("/Users/arul/.npm-global/bin");
334+
expect(codex).toEqual({
335+
cli: "codex",
336+
installed: true,
337+
path: "/Users/arul/.npm-global/bin/codex",
338+
authenticated: true,
339+
verified: true,
340+
});
341+
});
342+
239343
it("verifies API keys with provider endpoints", async () => {
240344
vi.stubGlobal(
241345
"fetch",

0 commit comments

Comments
 (0)