Run Biome formatter and add formatting guidance to CLAUDE.md
This commit is contained in:
@@ -35,6 +35,8 @@ npm run dev # Start daemon + Expo in Tmux
|
|||||||
npm run cli -- ls -a -g # List all agents
|
npm run cli -- ls -a -g # List all agents
|
||||||
npm run cli -- daemon status # Check daemon status
|
npm run cli -- daemon status # Check daemon status
|
||||||
npm run typecheck # Always run after changes
|
npm run typecheck # Always run after changes
|
||||||
|
npm run format # Auto-format with Biome
|
||||||
|
npm run format:check # Check formatting without writing
|
||||||
```
|
```
|
||||||
|
|
||||||
See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requirements, and debugging.
|
See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requirements, and debugging.
|
||||||
@@ -45,6 +47,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requir
|
|||||||
- **NEVER assume a timeout means the service needs restarting** — timeouts can be transient.
|
- **NEVER assume a timeout means the service needs restarting** — timeouts can be transient.
|
||||||
- **NEVER add auth checks to tests** — agent providers handle their own auth.
|
- **NEVER add auth checks to tests** — agent providers handle their own auth.
|
||||||
- **Always run typecheck after every change.**
|
- **Always run typecheck after every change.**
|
||||||
|
- **Run `npm run format` before committing.** This repo uses Biome for formatting. Do not manually fix formatting — let the formatter handle it.
|
||||||
- **NEVER make breaking changes to WebSocket or message schemas.** The primary compatibility path is old mobile app clients talking to newly updated daemons. Users update desktop and daemon first, then keep running the old app for a while. Every schema change MUST be backward-compatible for old clients against new daemons:
|
- **NEVER make breaking changes to WebSocket or message schemas.** The primary compatibility path is old mobile app clients talking to newly updated daemons. Users update desktop and daemon first, then keep running the old app for a while. Every schema change MUST be backward-compatible for old clients against new daemons:
|
||||||
- New fields: always `.optional()` with a sensible default or `.transform()` fallback.
|
- New fields: always `.optional()` with a sensible default or `.transform()` fallback.
|
||||||
- Never change a field from optional to required.
|
- Never change a field from optional to required.
|
||||||
|
|||||||
@@ -37,18 +37,17 @@ describe("splitMarkdownBlocks", () => {
|
|||||||
|
|
||||||
it("splits mixed paragraph, code fence, and paragraph content into three blocks", () => {
|
it("splits mixed paragraph, code fence, and paragraph content into three blocks", () => {
|
||||||
expect(
|
expect(
|
||||||
splitMarkdownBlocks("Intro paragraph\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nOutro paragraph"),
|
splitMarkdownBlocks(
|
||||||
).toEqual([
|
"Intro paragraph\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nOutro paragraph",
|
||||||
"Intro paragraph",
|
),
|
||||||
"```ts\nconst a = 1;\n\nconst b = 2;\n```",
|
).toEqual(["Intro paragraph", "```ts\nconst a = 1;\n\nconst b = 2;\n```", "Outro paragraph"]);
|
||||||
"Outro paragraph",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps everything from an unclosed fence start as one block for streaming content", () => {
|
it("keeps everything from an unclosed fence start as one block for streaming content", () => {
|
||||||
expect(
|
expect(splitMarkdownBlocks("Before fence\n\n```ts\nconst a = 1;\n\nconst b = 2;")).toEqual([
|
||||||
splitMarkdownBlocks("Before fence\n\n```ts\nconst a = 1;\n\nconst b = 2;"),
|
"Before fence",
|
||||||
).toEqual(["Before fence", "```ts\nconst a = 1;\n\nconst b = 2;"]);
|
"```ts\nconst a = 1;\n\nconst b = 2;",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an empty array for empty input", () => {
|
it("returns an empty array for empty input", () => {
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ export function splitMarkdownBlocks(text: string): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (fenceDelimiter[0] === activeFenceCharacter && fenceDelimiter.length >= activeFenceLength) {
|
||||||
fenceDelimiter[0] === activeFenceCharacter &&
|
|
||||||
fenceDelimiter.length >= activeFenceLength
|
|
||||||
) {
|
|
||||||
activeFenceCharacter = null;
|
activeFenceCharacter = null;
|
||||||
activeFenceLength = 0;
|
activeFenceLength = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ function createWorkspaceRuntimeSnapshot(
|
|||||||
...overrides?.github,
|
...overrides?.github,
|
||||||
pullRequest:
|
pullRequest:
|
||||||
overrides?.github && "pullRequest" in overrides.github
|
overrides?.github && "pullRequest" in overrides.github
|
||||||
? overrides.github.pullRequest ?? null
|
? (overrides.github.pullRequest ?? null)
|
||||||
: base.github.pullRequest,
|
: base.github.pullRequest,
|
||||||
error:
|
error:
|
||||||
overrides?.github && "error" in overrides.github
|
overrides?.github && "error" in overrides.github
|
||||||
? overrides.github.error ?? null
|
? (overrides.github.error ?? null)
|
||||||
: base.github.error,
|
: base.github.error,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -309,7 +309,9 @@ describe("workspace git watch targets", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo");
|
expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo");
|
||||||
expect(emitted.find((message) => message.type === "checkout_pr_status_response")?.payload).toEqual({
|
expect(
|
||||||
|
emitted.find((message) => message.type === "checkout_pr_status_response")?.payload,
|
||||||
|
).toEqual({
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
status: {
|
status: {
|
||||||
url: "https://github.com/acme/repo/pull/456",
|
url: "https://github.com/acme/repo/pull/456",
|
||||||
@@ -366,7 +368,9 @@ describe("workspace git watch targets", () => {
|
|||||||
expect(workspaceGitService.refresh).toHaveBeenCalledWith("/tmp/repo", {
|
expect(workspaceGitService.refresh).toHaveBeenCalledWith("/tmp/repo", {
|
||||||
priority: "high",
|
priority: "high",
|
||||||
});
|
});
|
||||||
expect(emitted.find((message) => message.type === "checkout_pr_status_response")?.payload).toEqual({
|
expect(
|
||||||
|
emitted.find((message) => message.type === "checkout_pr_status_response")?.payload,
|
||||||
|
).toEqual({
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
status: {
|
status: {
|
||||||
url: "https://github.com/acme/repo/pull/457",
|
url: "https://github.com/acme/repo/pull/457",
|
||||||
|
|||||||
@@ -165,20 +165,22 @@ function createWorkspaceRuntimeSnapshot(
|
|||||||
...overrides?.github,
|
...overrides?.github,
|
||||||
pullRequest:
|
pullRequest:
|
||||||
overrides?.github && "pullRequest" in overrides.github
|
overrides?.github && "pullRequest" in overrides.github
|
||||||
? overrides.github.pullRequest ?? null
|
? (overrides.github.pullRequest ?? null)
|
||||||
: base.github.pullRequest,
|
: base.github.pullRequest,
|
||||||
error:
|
error:
|
||||||
overrides?.github && "error" in overrides.github
|
overrides?.github && "error" in overrides.github
|
||||||
? overrides.github.error ?? null
|
? (overrides.github.error ?? null)
|
||||||
: base.github.error,
|
: base.github.error,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSessionForWorkspaceTests(options: {
|
function createSessionForWorkspaceTests(
|
||||||
appVersion?: string | null;
|
options: {
|
||||||
workspaceGitService?: ReturnType<typeof createNoopWorkspaceGitService>;
|
appVersion?: string | null;
|
||||||
} = {}): Session {
|
workspaceGitService?: ReturnType<typeof createNoopWorkspaceGitService>;
|
||||||
|
} = {},
|
||||||
|
): Session {
|
||||||
const logger = {
|
const logger = {
|
||||||
child: () => logger,
|
child: () => logger,
|
||||||
trace: vi.fn(),
|
trace: vi.fn(),
|
||||||
@@ -238,8 +240,8 @@ function createSessionForWorkspaceTests(options: {
|
|||||||
checkoutDiffWatcherCount: 0,
|
checkoutDiffWatcherCount: 0,
|
||||||
checkoutDiffFallbackRefreshTargetCount: 0,
|
checkoutDiffFallbackRefreshTargetCount: 0,
|
||||||
}),
|
}),
|
||||||
dispose: () => {},
|
dispose: () => {},
|
||||||
} as any,
|
} as any,
|
||||||
workspaceGitService: (options.workspaceGitService ?? createNoopWorkspaceGitService()) as any,
|
workspaceGitService: (options.workspaceGitService ?? createNoopWorkspaceGitService()) as any,
|
||||||
mcpBaseUrl: null,
|
mcpBaseUrl: null,
|
||||||
stt: null,
|
stt: null,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import type {
|
import type { CheckoutStatusGit, PullRequestStatusResult } from "../utils/checkout-git.js";
|
||||||
CheckoutStatusGit,
|
|
||||||
PullRequestStatusResult,
|
|
||||||
} from "../utils/checkout-git.js";
|
|
||||||
import {
|
import {
|
||||||
WorkspaceGitServiceImpl,
|
WorkspaceGitServiceImpl,
|
||||||
type WorkspaceGitRuntimeSnapshot,
|
type WorkspaceGitRuntimeSnapshot,
|
||||||
@@ -65,11 +62,11 @@ function createSnapshot(
|
|||||||
...overrides?.github,
|
...overrides?.github,
|
||||||
pullRequest:
|
pullRequest:
|
||||||
overrides?.github && "pullRequest" in overrides.github
|
overrides?.github && "pullRequest" in overrides.github
|
||||||
? overrides.github.pullRequest ?? null
|
? (overrides.github.pullRequest ?? null)
|
||||||
: base.github.pullRequest,
|
: base.github.pullRequest,
|
||||||
error:
|
error:
|
||||||
overrides?.github && "error" in overrides.github
|
overrides?.github && "error" in overrides.github
|
||||||
? overrides.github.error ?? null
|
? (overrides.github.error ?? null)
|
||||||
: base.github.error,
|
: base.github.error,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -139,7 +136,7 @@ function createService(options?: {
|
|||||||
logger: createLogger() as any,
|
logger: createLogger() as any,
|
||||||
paseoHome: "/tmp/paseo-test",
|
paseoHome: "/tmp/paseo-test",
|
||||||
deps: {
|
deps: {
|
||||||
watch: options?.watch ?? (((() => createWatcher()) as unknown) as any),
|
watch: options?.watch ?? ((() => createWatcher()) as unknown as any),
|
||||||
getCheckoutStatus:
|
getCheckoutStatus:
|
||||||
options?.getCheckoutStatus ?? vi.fn(async (cwd: string) => createCheckoutStatus(cwd)),
|
options?.getCheckoutStatus ?? vi.fn(async (cwd: string) => createCheckoutStatus(cwd)),
|
||||||
getCheckoutShortstat:
|
getCheckoutShortstat:
|
||||||
@@ -149,8 +146,7 @@ function createService(options?: {
|
|||||||
deletions: 0,
|
deletions: 0,
|
||||||
})),
|
})),
|
||||||
getPullRequestStatus:
|
getPullRequestStatus:
|
||||||
options?.getPullRequestStatus ??
|
options?.getPullRequestStatus ?? vi.fn(async () => createPullRequestStatusResult()),
|
||||||
vi.fn(async () => createPullRequestStatusResult()),
|
|
||||||
resolveGhPath: options?.resolveGhPath ?? vi.fn(async () => "/usr/bin/gh"),
|
resolveGhPath: options?.resolveGhPath ?? vi.fn(async () => "/usr/bin/gh"),
|
||||||
resolveAbsoluteGitDir: options?.resolveAbsoluteGitDir ?? vi.fn(async () => "/tmp/repo/.git"),
|
resolveAbsoluteGitDir: options?.resolveAbsoluteGitDir ?? vi.fn(async () => "/tmp/repo/.git"),
|
||||||
hasOriginRemote: options?.hasOriginRemote ?? vi.fn(async () => false),
|
hasOriginRemote: options?.hasOriginRemote ?? vi.fn(async () => false),
|
||||||
@@ -325,10 +321,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const nowValues = [
|
const nowValues = [new Date("2026-04-12T00:00:00.000Z"), new Date("2026-04-12T00:05:00.000Z")];
|
||||||
new Date("2026-04-12T00:00:00.000Z"),
|
|
||||||
new Date("2026-04-12T00:05:00.000Z"),
|
|
||||||
];
|
|
||||||
const service = createService({
|
const service = createService({
|
||||||
getPullRequestStatus,
|
getPullRequestStatus,
|
||||||
now: () => nowValues.shift() ?? new Date("2026-04-12T00:05:00.000Z"),
|
now: () => nowValues.shift() ?? new Date("2026-04-12T00:05:00.000Z"),
|
||||||
@@ -383,20 +376,18 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
aheadOfOrigin: 2,
|
aheadOfOrigin: 2,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const getPullRequestStatus = vi
|
const getPullRequestStatus = vi.fn<() => Promise<PullRequestStatusResult>>().mockResolvedValue(
|
||||||
.fn<() => Promise<PullRequestStatusResult>>()
|
createPullRequestStatusResult({
|
||||||
.mockResolvedValue(
|
status: {
|
||||||
createPullRequestStatusResult({
|
url: "https://github.com/acme/repo/pull/123",
|
||||||
status: {
|
title: "Runtime payloads",
|
||||||
url: "https://github.com/acme/repo/pull/123",
|
state: "open",
|
||||||
title: "Runtime payloads",
|
baseRefName: "main",
|
||||||
state: "open",
|
headRefName: "feature/runtime-payloads",
|
||||||
baseRefName: "main",
|
isMerged: false,
|
||||||
headRefName: "feature/runtime-payloads",
|
},
|
||||||
isMerged: false,
|
}),
|
||||||
},
|
);
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const service = createService({
|
const service = createService({
|
||||||
getCheckoutStatus,
|
getCheckoutStatus,
|
||||||
|
|||||||
@@ -258,7 +258,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
this.scheduleWorkspaceRefresh(target);
|
this.scheduleWorkspaceRefresh(target);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn({ err: error, cwd: target.cwd, watchPath }, "Failed to start workspace git watcher");
|
this.logger.warn(
|
||||||
|
{ err: error, cwd: target.cwd, watchPath },
|
||||||
|
"Failed to start workspace git watcher",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!watcher) {
|
if (!watcher) {
|
||||||
@@ -332,7 +335,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
const snapshot = await this.refreshSnapshot(target.cwd);
|
const snapshot = await this.refreshSnapshot(target.cwd);
|
||||||
this.rememberSnapshot(target, snapshot, { notify: true });
|
this.rememberSnapshot(target, snapshot, { notify: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn({ err: error, cwd: target.cwd }, "Failed to refresh workspace git snapshot");
|
this.logger.warn(
|
||||||
|
{ err: error, cwd: target.cwd },
|
||||||
|
"Failed to refresh workspace git snapshot",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} while (target.refreshQueued);
|
} while (target.refreshQueued);
|
||||||
})();
|
})();
|
||||||
@@ -378,7 +384,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
target.fetchInFlight = true;
|
target.fetchInFlight = true;
|
||||||
this.logger.debug({ repoGitRoot: target.repoGitRoot, cwd: target.cwd }, "Running background git fetch");
|
this.logger.debug(
|
||||||
|
{ repoGitRoot: target.repoGitRoot, cwd: target.cwd },
|
||||||
|
"Running background git fetch",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.deps.runGitFetch(target.cwd);
|
await this.deps.runGitFetch(target.cwd);
|
||||||
|
|||||||
Reference in New Issue
Block a user