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: 2 additions & 2 deletions .ade/ade.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ processes:
- id: dbun9idy
name: dogfood code review
command:
- /Users/admin/Projects/ADE/scripts/dogfood.sh
- scripts/dogfood.sh
- code-review
cwd: /Users/admin/Projects/ADE
cwd: .
gracefulShutdownMs: 7000
stackButtons: []
testSuites: []
Expand Down
58 changes: 57 additions & 1 deletion .claude/commands/finalize.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The only outputs are the Phase 4 summary and any error messages for genuinely fa

```
Phase 1: Analyze code changes and batch simplification work (lead)
Phase 2: Parallel execution (simplify + docs) (agents)
Phase 2: Parallel execution (simplify + docs + mobile parity)(agents)
Phase 3: CI sync + local verification (lead)
Phase 4: Summary (lead)
```
Expand Down Expand Up @@ -185,6 +185,56 @@ This validator only covers the Mintlify site. For internal docs, self-check:
Report what docs were updated and what was changed.
```

### Mobile parity agent

Spawn a general-purpose agent with this prompt:

```
You are the mobile parity reviewer for the ADE project.

Analyze all work on the current branch vs main, including changes that are
already under review and any simplifications made during `/finalize`. Determine
whether the iOS companion app under `apps/ios/` needs matching updates.

Step 1: Get branch context
git diff main --name-only
git diff main --stat | tail -30
git log main..HEAD --oneline

Step 2: Identify cross-platform changes
- Shared contracts: apps/desktop/src/shared/**, preload IPC types, sync payloads,
PR mobile snapshots, chat/session models, lane summaries, config schemas.
- Desktop behavior with a mobile surface: PR workflows, lanes, Work chat,
files, sync/multi-device, settings exposed on iOS, model/session controls.
- Renderer-only desktop preferences are only mobile-applicable when the iOS app
has the same user-facing concept and a native implementation path.

Step 3: Inspect iOS equivalents
- Search `apps/ios/ADE` and `apps/ios/ADETests` for the affected model, view,
service, or workflow names.
- If the branch adds or changes a host/mobile contract, update Swift Codable
models and iOS tests as needed.
- If the branch changes user-facing behavior that iOS already exposes, update
the SwiftUI view using native iOS controls and existing ADE design patterns.
- If the change is not applicable to iOS, explain why in the report.

Step 4: Apply required iOS updates
- Keep edits scoped to `apps/ios/` unless a shared contract fix is required.
- Prefer existing SwiftUI patterns and native controls.
- Preserve Dynamic Type, VoiceOver labels, and 44x44 tap targets.
- Add or update targeted tests in `apps/ios/ADETests` for contract changes.

Step 5: Validate what you touched
- At minimum: `xcrun swiftc -parse <changed swift files>` when a full Xcode
build/test run is unavailable.
- Prefer an iOS build/test when the local simulator/runtime environment supports it.

Report:
- iOS files changed, or "No iOS changes required"
- Why each desktop/shared change was applicable or not applicable to mobile
- Validation run and any environment limitations
```

Wait for all agents to complete.

---
Expand Down Expand Up @@ -326,6 +376,11 @@ If Phase 3e fails only inside files the simplifier touched, revert the simplifie
- Docs checked but unchanged: [list]
- Doc validation: PASS

### Mobile Parity:
- iOS changes: [list or "none required"]
- Applicability notes: [brief list]
- Validation: PASS / blocked with reason

### CI Verification:
- Lock files in sync: PASS
- Typecheck (desktop): PASS
Expand All @@ -347,6 +402,7 @@ If Phase 3e fails only inside files the simplifier touched, revert the simplifie
Before marking complete:
- [ ] Code simplification completed on all batches
- [ ] Documentation updated for all affected areas
- [ ] Mobile parity reviewed; applicable iOS updates made and validated
- [ ] CI workflow sync verified (no orphaned test files)
- [ ] Lock files in sync (no dirty lock files after install)
- [ ] Typecheck passed (desktop + mcp-server + web)
Expand Down
98 changes: 88 additions & 10 deletions apps/desktop/src/main/services/ai/tools/grepSearch.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createGrepSearchTool } from "./grepSearch";
import {
__testResetRipgrepExecFile,
__testSetRipgrepExecFile,
createGrepSearchTool,
} from "./grepSearch";

const tmpDirs: string[] = [];
function makeTmpDir(prefix: string): string {
Expand All @@ -18,6 +23,7 @@ function writeFixtureFile(root: string, relativePath: string, content: string):
}

afterEach(() => {
__testResetRipgrepExecFile();
vi.restoreAllMocks();
for (const dir of tmpDirs) {
try {
Expand All @@ -29,22 +35,19 @@ afterEach(() => {
tmpDirs.length = 0;
});

// Helper to force JS fallback by making execFile reject for rg
// Force JS fallback by making ripgrep's exec path reject (matches real "rg missing" behavior).
function forceJsFallback(): void {
const cp = require("node:child_process");
const originalExecFile = cp.execFile;
vi.spyOn(cp, "execFile").mockImplementation(
(cmd: unknown, ...rest: unknown[]) => {
__testSetRipgrepExecFile(
((cmd: unknown, ...rest: unknown[]) => {
if (cmd === "rg") {
// Make the promisified version reject
const cb = rest[rest.length - 1];
if (typeof cb === "function") {
process.nextTick(() => cb(new Error("rg not available")));
process.nextTick(() => (cb as (err: Error) => void)(new Error("rg not available")));
return;
}
}
return originalExecFile(cmd, ...rest);
},
return (execFile as (typeof import("node:child_process"))["execFile"])(cmd as never, ...rest as never[]);
}) as typeof execFile,
);
}

Expand Down Expand Up @@ -199,6 +202,19 @@ describe("createGrepSearchTool", () => {
expect(result.matches[0].displayPath).toBe("src/app.ts");
});

it("repo-wide JS fallback skips root .ade but still searches .github", async () => {
const cwd = makeTmpDir("grep-hidden-root-");
writeFixtureFile(cwd, ".ade/secrets.txt", "SECRET_MARKER");
writeFixtureFile(cwd, ".github/workflows/ci.yml", "SECRET_MARKER");
writeFixtureFile(cwd, "src/app.ts", "SECRET_MARKER");
forceJsFallback();

const tool = createGrepSearchTool(cwd);
const result = await tool.execute({ pattern: "SECRET_MARKER", context: 0 });
const paths = result.matches.map((m) => m.displayPath).sort();
expect(paths).toEqual([".github/workflows/ci.yml", "src/app.ts"]);
});

it("handles brace expansion in file glob: *.{ts,tsx}", async () => {
const cwd = makeTmpDir("grep-brace-");
writeFixtureFile(cwd, "app.ts", "const val = 1;");
Expand Down Expand Up @@ -254,6 +270,68 @@ describe("createGrepSearchTool", () => {
expect(result.error).toBeDefined();
expect(result.matchCount).toBe(0);
});

it("surfaces a descriptive 'Invalid regex pattern' error for malformed patterns (JS fallback)", async () => {
const cwd = makeTmpDir("grep-bad-regex-");
writeFixtureFile(cwd, "code.ts", "const x = 1;");
forceJsFallback();

const tool = createGrepSearchTool(cwd);
// Unmatched `[` — a SyntaxError from `new RegExp`.
const result = await tool.execute({ pattern: "[", context: 0 });

expect(result.matchCount).toBe(0);
expect(result.error).toBeDefined();
expect(result.error).toContain("Invalid regex pattern");
});
});

// --------------------------------------------------------------------------
// Glob edge cases
// --------------------------------------------------------------------------

describe("glob handling", () => {
it("matches bare filenames under a **/*.ts glob (JS fallback)", async () => {
const cwd = makeTmpDir("grep-starstar-");
writeFixtureFile(cwd, "foo.ts", "const marker = 1;");
writeFixtureFile(cwd, "src/bar.ts", "const marker = 2;");
writeFixtureFile(cwd, "readme.md", "marker");
forceJsFallback();

const tool = createGrepSearchTool(cwd);
const result = await tool.execute({ pattern: "marker", glob: "**/*.ts", context: 0 });

const paths = result.matches.map((m) => m.displayPath).sort();
expect(paths).toEqual(["foo.ts", "src/bar.ts"]);
});

it("preserves directory components in JS fallback globs", async () => {
const cwd = makeTmpDir("grep-dir-glob-");
writeFixtureFile(cwd, "src/app.ts", "const marker = 1;");
writeFixtureFile(cwd, "src/deep/app.ts", "const marker = 2;");
writeFixtureFile(cwd, "lib/app.ts", "const marker = 3;");
forceJsFallback();

const tool = createGrepSearchTool(cwd);
const result = await tool.execute({ pattern: "marker", glob: "src/*.ts", context: 0 });

const paths = result.matches.map((m) => m.displayPath).sort();
expect(paths).toEqual(["src/app.ts"]);
});

it("matches directory ** globs without escaping the subtree", async () => {
const cwd = makeTmpDir("grep-dir-starstar-");
writeFixtureFile(cwd, "services/index.ts", "const marker = 1;");
writeFixtureFile(cwd, "services/api/handler.ts", "const marker = 2;");
writeFixtureFile(cwd, "packages/services/index.ts", "const marker = 3;");
forceJsFallback();

const tool = createGrepSearchTool(cwd);
const result = await tool.execute({ pattern: "marker", glob: "services/**/*.ts", context: 0 });

const paths = result.matches.map((m) => m.displayPath).sort();
expect(paths).toEqual(["services/api/handler.ts", "services/index.ts"]);
});
});

// --------------------------------------------------------------------------
Expand Down
Loading
Loading