[codex] fix worktree dev dependency ergonomics (#3743)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Local development needs to work cleanly across linked git worktrees
because Paperclip itself leans on worktree-based engineering workflows
> - Dev-mode asset routing, Vite watch behavior, and workspace package
links are part of that day-to-day control-plane ergonomics
> - The current branch had a small but coherent set of
worktree/dev-tooling fixes that are independent from both the issue UI
changes and the heartbeat runtime changes
> - This pull request isolates those environment fixes into a standalone
branch that can merge without carrying unrelated product work
> - The benefit is a smoother multi-worktree developer loop with fewer
stale links and less noisy dev watching

## What Changed

- Serve dev public assets before the HTML shell and add a routing test
that locks that behavior in.
- Ignore UI test files in the Vite dev watch helper so the dev server
does less unnecessary work.
- Update `ensure-workspace-package-links.ts` to relink stale workspace
dependencies whenever a workspace `node_modules` directory exists,
instead of only inside linked-worktree detection paths.

## Verification

- `pnpm vitest run server/src/__tests__/app-vite-dev-routing.test.ts
ui/src/lib/vite-watch.test.ts`
- `node cli/node_modules/tsx/dist/cli.mjs
scripts/ensure-workspace-package-links.ts`

## Risks

- The asset routing change is low risk but sits near app shell behavior,
so a regression would show up as broken static assets in dev mode.
- The workspace-link repair now runs in more cases, so the main risk is
doing unexpected relinks when a checkout has intentionally unusual
workspace symlink state.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Dotta 2026-04-15 09:47:29 -05:00 committed by GitHub
parent 390502736c
commit c1a02497b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 101 additions and 18 deletions

View file

@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import type { Request } from "express";
import { shouldServeViteDevHtml } from "../app.js";
function createRequest(path: string, acceptsResult: string | false): Request {
return {
path,
accepts: () => acceptsResult,
} as unknown as Request;
}
describe("shouldServeViteDevHtml", () => {
it("serves HTML shell for document requests", () => {
expect(shouldServeViteDevHtml(createRequest("/", "html"))).toBe(true);
expect(shouldServeViteDevHtml(createRequest("/issues/abc", "html"))).toBe(true);
});
it("skips public assets even when the client accepts */*", () => {
expect(shouldServeViteDevHtml(createRequest("/sw.js", "html"))).toBe(false);
expect(shouldServeViteDevHtml(createRequest("/site.webmanifest", "html"))).toBe(false);
});
it("skips vite asset requests", () => {
expect(shouldServeViteDevHtml(createRequest("/@vite/client", "html"))).toBe(false);
expect(shouldServeViteDevHtml(createRequest("/src/main.tsx", "html"))).toBe(false);
});
});

View file

@ -56,6 +56,7 @@ describe("shouldSilenceHttpSuccessLog", () => {
expect(shouldSilenceHttpSuccessLog("GET", "/@fs/Users/dotta/paperclip/ui/src/main.tsx", 200)).toBe(true);
expect(shouldSilenceHttpSuccessLog("GET", "/src/App.tsx?t=123", 200)).toBe(true);
expect(shouldSilenceHttpSuccessLog("GET", "/site.webmanifest", 200)).toBe(true);
expect(shouldSilenceHttpSuccessLog("GET", "/sw.js", 200)).toBe(true);
});
it("keeps normal successful application requests", () => {

View file

@ -70,6 +70,7 @@ const VITE_DEV_STATIC_PATHS = new Set([
"/favicon.ico",
"/favicon.svg",
"/site.webmanifest",
"/sw.js",
]);
export function resolveViteHmrPort(serverPort: number): number {
@ -79,7 +80,7 @@ export function resolveViteHmrPort(serverPort: number): number {
return Math.max(1_024, serverPort - 10_000);
}
function shouldServeViteDevHtml(req: ExpressRequest): boolean {
export function shouldServeViteDevHtml(req: ExpressRequest): boolean {
const pathname = req.path;
if (VITE_DEV_STATIC_PATHS.has(pathname)) return false;
if (VITE_DEV_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix))) return false;
@ -347,6 +348,7 @@ export async function createApp(
if (opts.uiMode === "vite-dev") {
const uiRoot = path.resolve(__dirname, "../../ui");
const publicUiRoot = path.resolve(uiRoot, "public");
const hmrPort = resolveViteHmrPort(opts.serverPort);
const { createServer: createViteServer } = await import("vite");
const vite = await createViteServer({
@ -369,6 +371,9 @@ export async function createApp(
});
const renderViteHtml = viteHtmlRenderer;
if (fs.existsSync(publicUiRoot)) {
app.use(express.static(publicUiRoot, { index: false }));
}
app.get(/.*/, async (req, res, next) => {
if (!shouldServeViteDevHtml(req)) {
next();

View file

@ -25,6 +25,7 @@ const SILENCED_SUCCESS_STATIC_PREFIXES = [
const SILENCED_SUCCESS_STATIC_PATHS = new Set([
"/favicon.ico",
"/site.webmanifest",
"/sw.js",
]);
function normalizePath(url: string): string {