Skip to content

Commit 8751bbc

Browse files
authored
Merge pull request #85 from arul28/merge
runnign automate and finalzie
2 parents 02b4aa1 + e2818b8 commit 8751bbc

93 files changed

Lines changed: 7590 additions & 1603 deletions

File tree

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
strategy:
9696
fail-fast: false
9797
matrix:
98-
shard: [1, 2, 3]
98+
shard: [1, 2, 3, 4, 5]
9999
steps:
100100
- uses: actions/checkout@v4
101101
- uses: actions/setup-node@v4
@@ -108,7 +108,7 @@ jobs:
108108
apps/mcp-server/node_modules
109109
apps/web/node_modules
110110
key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/mcp-server/package-lock.json','apps/web/package-lock.json') }}
111-
- run: cd apps/desktop && npx vitest run --shard=${{ matrix.shard }}/3
111+
- run: cd apps/desktop && npx vitest run --shard=${{ matrix.shard }}/5
112112

113113
test-mcp:
114114
needs: install

apps/desktop/src/main/main.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { createMissionMemoryLifecycleService } from "./services/memory/missionMe
6969
import { createEpisodicSummaryService } from "./services/memory/episodicSummaryService";
7070
import { createHumanWorkDigestService } from "./services/memory/humanWorkDigestService";
7171
import { createProceduralLearningService } from "./services/memory/proceduralLearningService";
72+
import { createMemoryRepairService } from "./services/memory/memoryRepairService";
7273
import { createSkillRegistryService } from "./services/memory/skillRegistryService";
7374
import { createKnowledgeCaptureService } from "./services/memory/knowledgeCaptureService";
7475
import { createCtoStateService } from "./services/cto/ctoStateService";
@@ -954,6 +955,8 @@ app.whenReady().then(async () => {
954955
logger,
955956
cacheDir: path.join(app.getPath("userData"), "transformers-cache"),
956957
});
958+
// Auto-detect previously downloaded embedding model at startup
959+
void embeddingService.probeCache().catch(() => { /* best-effort */ });
957960
const hybridSearchService = createHybridSearchService({
958961
db,
959962
embeddingService,
@@ -995,6 +998,11 @@ app.whenReady().then(async () => {
995998
projectId,
996999
onStatus: (event) => emitProjectEvent(projectRoot, IPC.memorySweepStatus, event)
9971000
});
1001+
const memoryRepairService = createMemoryRepairService({
1002+
db,
1003+
projectId,
1004+
logger,
1005+
});
9981006
const embeddingWorkerService = createEmbeddingWorkerService({
9991007
db,
10001008
logger,
@@ -1107,6 +1115,15 @@ app.whenReady().then(async () => {
11071115
error: error instanceof Error ? error.message : String(error),
11081116
});
11091117
}
1118+
1119+
try {
1120+
memoryRepairService.runRepair();
1121+
} catch (error) {
1122+
logger.warn("memory.repair.failed", {
1123+
projectRoot,
1124+
error: error instanceof Error ? error.message : String(error),
1125+
});
1126+
}
11101127
});
11111128

11121129
const workerRevisionService = createWorkerRevisionService({
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it } from "vitest";
2+
import { resolveClaudeCodeExecutable } from "./claudeCodeExecutable";
3+
4+
describe("resolveClaudeCodeExecutable", () => {
5+
it("prefers the explicit env override", () => {
6+
expect(
7+
resolveClaudeCodeExecutable({
8+
env: {
9+
CLAUDE_CODE_EXECUTABLE_PATH: "/custom/bin/claude",
10+
PATH: "/usr/bin:/bin",
11+
},
12+
}),
13+
).toEqual({
14+
path: "/custom/bin/claude",
15+
source: "env",
16+
});
17+
});
18+
19+
it("uses the detected Claude auth path before falling back to PATH lookup", () => {
20+
expect(
21+
resolveClaudeCodeExecutable({
22+
auth: [
23+
{
24+
type: "cli-subscription",
25+
cli: "claude",
26+
path: "/opt/homebrew/bin/claude",
27+
authenticated: true,
28+
verified: true,
29+
},
30+
],
31+
env: {
32+
PATH: "/usr/bin:/bin",
33+
},
34+
}),
35+
).toEqual({
36+
path: "/opt/homebrew/bin/claude",
37+
source: "auth",
38+
});
39+
});
40+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import type { DetectedAuth } from "./authDetector";
5+
6+
export type ClaudeCodeExecutableResolution = {
7+
path: string;
8+
source: "env" | "auth" | "path" | "common-dir" | "fallback-command";
9+
};
10+
11+
const HOME_DIR = os.homedir();
12+
const COMMON_BIN_DIRS = [
13+
"/opt/homebrew/bin",
14+
"/opt/homebrew/sbin",
15+
"/usr/local/bin",
16+
"/usr/local/sbin",
17+
"/usr/bin",
18+
"/bin",
19+
`${HOME_DIR}/.local/bin`,
20+
`${HOME_DIR}/.nvm/current/bin`,
21+
].filter(Boolean);
22+
23+
function isExecutableFile(candidatePath: string): boolean {
24+
try {
25+
const stat = fs.statSync(candidatePath);
26+
return stat.isFile() && (process.platform === "win32" || (stat.mode & 0o111) !== 0);
27+
} catch {
28+
return false;
29+
}
30+
}
31+
32+
function resolveFromPathEntries(command: string, pathValue: string | undefined): string | null {
33+
if (!pathValue) return null;
34+
for (const entry of pathValue.split(path.delimiter)) {
35+
const trimmed = entry.trim();
36+
if (!trimmed) continue;
37+
const candidatePath = path.join(trimmed, command);
38+
if (isExecutableFile(candidatePath)) {
39+
return candidatePath;
40+
}
41+
}
42+
return null;
43+
}
44+
45+
function findClaudeAuthPath(auth?: DetectedAuth[]): string | null {
46+
for (const entry of auth ?? []) {
47+
if (entry.type !== "cli-subscription" || entry.cli !== "claude") continue;
48+
const candidate = entry.path.trim();
49+
if (candidate) {
50+
return candidate;
51+
}
52+
}
53+
return null;
54+
}
55+
56+
export function resolveClaudeCodeExecutable(args?: {
57+
auth?: DetectedAuth[];
58+
env?: NodeJS.ProcessEnv;
59+
}): ClaudeCodeExecutableResolution {
60+
const env = args?.env ?? process.env;
61+
const envPath = env.CLAUDE_CODE_EXECUTABLE_PATH?.trim();
62+
if (envPath) {
63+
return { path: envPath, source: "env" };
64+
}
65+
66+
const authPath = findClaudeAuthPath(args?.auth);
67+
if (authPath) {
68+
return { path: authPath, source: "auth" };
69+
}
70+
71+
const pathResolved = resolveFromPathEntries("claude", env.PATH);
72+
if (pathResolved) {
73+
return { path: pathResolved, source: "path" };
74+
}
75+
76+
for (const binDir of COMMON_BIN_DIRS) {
77+
const candidatePath = path.join(binDir, "claude");
78+
if (isExecutableFile(candidatePath)) {
79+
return { path: candidatePath, source: "common-dir" };
80+
}
81+
}
82+
83+
return { path: "claude", source: "fallback-command" };
84+
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ const mockState = vi.hoisted(() => ({
55
reportProviderRuntimeReady: vi.fn(),
66
reportProviderRuntimeAuthFailure: vi.fn(),
77
reportProviderRuntimeFailure: vi.fn(),
8+
resolveClaudeCodeExecutable: vi.fn(() => ({ path: "/usr/local/bin/claude", source: "path" })),
9+
normalizeCliMcpServers: vi.fn(() => ({
10+
ade: {
11+
type: "stdio",
12+
command: "node",
13+
args: ["probe.js"],
14+
env: { ADE_PROJECT_ROOT: "/tmp/project" },
15+
},
16+
})),
17+
resolveAdeMcpServerLaunch: vi.fn(() => ({
18+
command: "node",
19+
cmdArgs: ["probe.js"],
20+
env: { ADE_PROJECT_ROOT: "/tmp/project" },
21+
})),
822
}));
923

1024
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
@@ -17,6 +31,18 @@ vi.mock("./providerRuntimeHealth", () => ({
1731
reportProviderRuntimeFailure: (...args: unknown[]) => mockState.reportProviderRuntimeFailure(...args),
1832
}));
1933

34+
vi.mock("./claudeCodeExecutable", () => ({
35+
resolveClaudeCodeExecutable: mockState.resolveClaudeCodeExecutable,
36+
}));
37+
38+
vi.mock("./providerResolver", () => ({
39+
normalizeCliMcpServers: mockState.normalizeCliMcpServers,
40+
}));
41+
42+
vi.mock("../orchestrator/unifiedOrchestratorAdapter", () => ({
43+
resolveAdeMcpServerLaunch: mockState.resolveAdeMcpServerLaunch,
44+
}));
45+
2046
let probeClaudeRuntimeHealth: typeof import("./claudeRuntimeProbe").probeClaudeRuntimeHealth;
2147
let resetClaudeRuntimeProbeCache: typeof import("./claudeRuntimeProbe").resetClaudeRuntimeProbeCache;
2248
let isClaudeRuntimeAuthError: typeof import("./claudeRuntimeProbe").isClaudeRuntimeAuthError;
@@ -40,6 +66,9 @@ beforeEach(async () => {
4066
mockState.reportProviderRuntimeReady.mockReset();
4167
mockState.reportProviderRuntimeAuthFailure.mockReset();
4268
mockState.reportProviderRuntimeFailure.mockReset();
69+
mockState.resolveClaudeCodeExecutable.mockClear();
70+
mockState.normalizeCliMcpServers.mockClear();
71+
mockState.resolveAdeMcpServerLaunch.mockClear();
4372
const mod = await import("./claudeRuntimeProbe");
4473
probeClaudeRuntimeHealth = mod.probeClaudeRuntimeHealth;
4574
resetClaudeRuntimeProbeCache = mod.resetClaudeRuntimeProbeCache;
@@ -66,6 +95,15 @@ describe("claudeRuntimeProbe", () => {
6695
expect(query.close).toHaveBeenCalledTimes(1);
6796
expect(mockState.reportProviderRuntimeAuthFailure).toHaveBeenCalledTimes(1);
6897
expect(mockState.reportProviderRuntimeFailure).not.toHaveBeenCalled();
98+
expect(mockState.query).toHaveBeenCalledWith(expect.objectContaining({
99+
options: expect.objectContaining({
100+
cwd: "/tmp/project",
101+
pathToClaudeCodeExecutable: "/usr/local/bin/claude",
102+
mcpServers: expect.objectContaining({
103+
ade: expect.any(Object),
104+
}),
105+
}),
106+
}));
69107
});
70108

71109
it("treats Anthropic 401 invalid credentials responses as auth failures", async () => {

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

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
13
import { query as claudeQuery, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
24
import type { Logger } from "../logging/logger";
35
import { getErrorMessage } from "../shared/utils";
6+
import { resolveAdeMcpServerLaunch } from "../orchestrator/unifiedOrchestratorAdapter";
47
import {
58
reportProviderRuntimeAuthFailure,
69
reportProviderRuntimeFailure,
710
reportProviderRuntimeReady,
811
} from "./providerRuntimeHealth";
12+
import { resolveClaudeCodeExecutable } from "./claudeCodeExecutable";
13+
import { normalizeCliMcpServers } from "./providerResolver";
914

1015
const PROBE_TIMEOUT_MS = 20_000;
1116
const PROBE_CACHE_TTL_MS = 30_000;
@@ -22,6 +27,7 @@ type ClaudeRuntimeProbeResult =
2227
/** Cache and in-flight probe keyed by projectRoot to avoid cross-project contamination. */
2328
const probeCache = new Map<string, { checkedAtMs: number; result: ClaudeRuntimeProbeResult }>();
2429
const inFlightProbes = new Map<string, Promise<ClaudeRuntimeProbeResult>>();
30+
let runtimeRootCache: string | null = null;
2531

2632
function normalizeErrorMessage(error: unknown): string {
2733
const text = getErrorMessage(error).trim();
@@ -84,16 +90,52 @@ function cacheResult(projectRoot: string, result: ClaudeRuntimeProbeResult): Cla
8490
return result;
8591
}
8692

87-
function publishResult(result: ClaudeRuntimeProbeResult): void {
88-
if (result.state === "ready") {
89-
reportProviderRuntimeReady("claude");
90-
return;
93+
function resolveProbeRuntimeRoot(): string {
94+
if (runtimeRootCache !== null) return runtimeRootCache;
95+
const startPoints = [process.cwd(), __dirname];
96+
for (const start of startPoints) {
97+
let dir = path.resolve(start);
98+
for (let i = 0; i < 12; i += 1) {
99+
if (fs.existsSync(path.join(dir, "apps", "mcp-server", "package.json"))) {
100+
runtimeRootCache = dir;
101+
return dir;
102+
}
103+
const parent = path.dirname(dir);
104+
if (parent === dir) break;
105+
dir = parent;
106+
}
91107
}
92-
if (result.state === "auth-failed") {
93-
reportProviderRuntimeAuthFailure("claude", result.message);
94-
return;
108+
runtimeRootCache = process.cwd();
109+
return runtimeRootCache;
110+
}
111+
112+
function resolveProbeMcpServers(projectRoot: string): Record<string, Record<string, unknown>> | undefined {
113+
const launch = resolveAdeMcpServerLaunch({
114+
workspaceRoot: projectRoot,
115+
runtimeRoot: resolveProbeRuntimeRoot(),
116+
defaultRole: "agent",
117+
});
118+
return normalizeCliMcpServers("claude", {
119+
ade: {
120+
command: launch.command,
121+
args: launch.cmdArgs,
122+
env: launch.env,
123+
},
124+
});
125+
}
126+
127+
function publishResult(result: ClaudeRuntimeProbeResult): void {
128+
switch (result.state) {
129+
case "ready":
130+
reportProviderRuntimeReady("claude");
131+
break;
132+
case "auth-failed":
133+
reportProviderRuntimeAuthFailure("claude", result.message);
134+
break;
135+
case "runtime-failed":
136+
reportProviderRuntimeFailure("claude", result.message);
137+
break;
95138
}
96-
reportProviderRuntimeFailure("claude", result.message);
97139
}
98140

99141
export function resetClaudeRuntimeProbeCache(): void {
@@ -119,20 +161,28 @@ export async function probeClaudeRuntimeHealth(args: {
119161
return;
120162
}
121163

164+
let claudeExecutablePath: string | null = null;
165+
122166
const probe = (async (): Promise<ClaudeRuntimeProbeResult> => {
123167
const abortController = new AbortController();
124168
const timeout = setTimeout(() => abortController.abort(), PROBE_TIMEOUT_MS);
125-
const stream = claudeQuery({
126-
prompt: "System initialization check. Respond with only the word READY.",
127-
options: {
128-
cwd: projectRoot,
129-
permissionMode: "plan",
130-
tools: [],
131-
abortController,
132-
},
133-
});
169+
let stream: ReturnType<typeof claudeQuery> | null = null;
134170

135171
try {
172+
const claudeExecutable = resolveClaudeCodeExecutable();
173+
claudeExecutablePath = claudeExecutable.path;
174+
stream = claudeQuery({
175+
prompt: "System initialization check. Respond with only the word READY.",
176+
options: {
177+
cwd: projectRoot,
178+
permissionMode: "plan",
179+
tools: [],
180+
pathToClaudeCodeExecutable: claudeExecutable.path,
181+
mcpServers: resolveProbeMcpServers(projectRoot) as any,
182+
abortController,
183+
},
184+
});
185+
136186
for await (const message of stream) {
137187
const result = resultFromSdkMessage(message);
138188
if (result) {
@@ -154,7 +204,7 @@ export async function probeClaudeRuntimeHealth(args: {
154204
} finally {
155205
clearTimeout(timeout);
156206
try {
157-
stream.close();
207+
stream?.close();
158208
} catch {
159209
// Best effort cleanup — avoid leaving the probe subprocess running.
160210
}
@@ -172,6 +222,7 @@ export async function probeClaudeRuntimeHealth(args: {
172222
projectRoot,
173223
state: result.state,
174224
message: result.message,
225+
claudeExecutablePath,
175226
});
176227
}
177228
} finally {

0 commit comments

Comments
 (0)