mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 07:12:00 +01:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors and agents prepare each new git worktree with `node
scripts/agent-preflight.mjs`
> - The server imports `@paperclipai/plugin-sdk` through its built
`dist`. The preflight did not build that `dist`.
> - On a fresh worktree, the server and the restart-chaos harness stop
at startup with `ERR_MODULE_NOT_FOUND` for
`server/node_modules/@paperclipai/plugin-sdk/dist/index.js`
> - The SDK exports `dist` on purpose: plugin workers load it under
plain Node. CI, `paperclipai run` and the typecheck scripts already
build it with `scripts/ensure-plugin-build-deps.mjs`.
> - This pull request makes the preflight run the same builder as a step
> - The benefit is that one preflight command gives a worktree that can
start the server and the harness
## Linked Issues or Issue Description
**What happened?**
After `node scripts/agent-preflight.mjs` on a fresh worktree, `node
cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts ...` stops
at startup. The error is `ERR_MODULE_NOT_FOUND` for
`server/node_modules/@paperclipai/plugin-sdk/dist/index.js`, imported
from `server/src/services/reusable-sandbox-resume-error.ts`. The
workaround is `pnpm --filter @paperclipai/plugin-sdk build`.
**Expected behavior**
The preflight prepares everything that the server needs to start. The
server and the harness import their modules without a manual build.
**Steps to reproduce**
1. Make a new worktree from `origin/master`.
2. Run `node scripts/agent-preflight.mjs`.
3. See that `packages/plugins/sdk/dist` does not exist.
4. From `server/`, import
`./src/services/reusable-sandbox-resume-error.ts` with tsx. The import
fails with `ERR_MODULE_NOT_FOUND`.
**Paperclip version or commit**
`origin/master` at a5e01634f6.
## What Changed
- `scripts/agent-preflight.mjs`: add a `plugin-sdk` step after install.
It runs `scripts/ensure-plugin-build-deps.mjs`. That script builds
`@paperclipai/shared` and `@paperclipai/plugin-sdk`, and it skips them
when their content fingerprints match the last build. The step reports
`built` or `skipped`. It fails when the builder fails or when a
completion marker is missing after the build.
- The SDK build does not use the shared cross-worktree cache. Both
packages compile in about 1.6 s with the native compiler, so a cache
clone does not save time.
- `scripts/__tests__/agent-preflight.test.mjs`: new tests. They use the
real builder script with a stand-in compiler. They cover a fresh build,
a skip when sources do not change, a rebuild after a source change, a
rebuild when only the SDK `dist` is missing, a failed build, and missing
outputs. The file is added to `test:release-registry` next to
`agent-build-cache.test.mjs`.
- `doc/DEVELOPING.md`: the "Agent worktree preflight" section describes
the new step.
I checked the other option: change the SDK exports to `src`, as most
sibling packages do. I did not do that. Plugin workers load the SDK
under plain Node, and other tools already depend on the built `dist`. I
also checked the other workspace packages that the server and the
harness import. Only `@paperclipai/paperclip-runner` and
`@paperclipai/paperclip-eval-kernel` also export `dist`, and the
preflight already builds them. The harness builds its sandbox plugin
fixtures itself (`tests/restart-chaos/plugin-build.ts`).
## Verification
- Reproduced on a fresh worktree from `origin/master`: after the
preflight, `packages/plugins/sdk/dist` did not exist and the tsx import
of `server/src/services/reusable-sandbox-resume-error.ts` failed with
`ERR_MODULE_NOT_FOUND`.
- On a second fresh worktree with this change: the preflight printed
`plugin-sdk: built in 1.6s`. A second run printed `plugin-sdk: skipped`.
A tsx import of `reusable-sandbox-resume-error.ts` and
`server/src/app.ts` passed. A tsx import of every module that
`tests/restart-chaos/main.ts` imports passed. I did not run a harness
wave.
- `node --test scripts/__tests__/agent-build-cache.test.mjs
scripts/__tests__/agent-preflight.test.mjs
scripts/__tests__/ensure-plugin-build-deps.test.mjs`: 23 pass.
## Risks
- Low risk. The change affects only the local agent preflight. A fresh
preflight takes about 2 s more. A current worktree adds one fingerprint
check (about 0.1 s).
- The step uses the builder's lock. If another build in the same
worktree holds the lock, the step waits for it, as the other callers do.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell
and file editing) in Claude Code.
## 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 checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
75 lines
3.5 KiB
JavaScript
75 lines
3.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { PLUGIN_SDK_BUILD_MARKERS, pluginSdkBuildStep } from "../agent-preflight.mjs";
|
|
|
|
// A worktree with the real ensure-plugin-build-deps script and a stand-in
|
|
// compiler that emits dist/index.js next to the tsconfig it is given.
|
|
function worktree(t) {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), "agent-preflight-"));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
mkdirSync(path.join(root, "scripts"));
|
|
copyFileSync(new URL("../ensure-plugin-build-deps.mjs", import.meta.url), path.join(root, "scripts/ensure-plugin-build-deps.mjs"));
|
|
const compiler = path.join(root, "node_modules/typescript/bin/tsc");
|
|
mkdirSync(path.dirname(compiler), { recursive: true });
|
|
writeFileSync(compiler, `
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const target = path.dirname(process.argv[3]);
|
|
if (fs.existsSync("fail")) process.exit(2);
|
|
fs.appendFileSync("builds", path.relative(process.cwd(), target) + "\\n");
|
|
fs.mkdirSync(path.join(target, "dist"), { recursive: true });
|
|
fs.writeFileSync(path.join(target, "dist/index.js"), "export {};\\n");
|
|
`);
|
|
for (const target of ["packages/shared", "packages/plugins/sdk"]) {
|
|
mkdirSync(path.join(root, target, "src"), { recursive: true });
|
|
writeFileSync(path.join(root, target, "src/index.ts"), "export {};\n");
|
|
writeFileSync(path.join(root, target, "tsconfig.json"), "{}");
|
|
}
|
|
const builds = () => existsSync(path.join(root, "builds"))
|
|
? readFileSync(path.join(root, "builds"), "utf8").trim().split("\n") : [];
|
|
return { root, builds };
|
|
}
|
|
|
|
test("a fresh worktree gets the plugin SDK and its shared dependency built", t => {
|
|
const { root, builds } = worktree(t);
|
|
const result = pluginSdkBuildStep(root);
|
|
assert.equal(result.step, "plugin-sdk");
|
|
assert.equal(result.action, "built");
|
|
assert.deepEqual(builds(), ["packages/shared", "packages/plugins/sdk"]);
|
|
for (const marker of PLUGIN_SDK_BUILD_MARKERS) assert.ok(existsSync(path.join(root, marker)), marker);
|
|
assert.ok(existsSync(path.join(root, "packages/plugins/sdk/dist/index.js")));
|
|
});
|
|
|
|
test("unchanged sources skip the build and a source change rebuilds", t => {
|
|
const { root, builds } = worktree(t);
|
|
pluginSdkBuildStep(root);
|
|
assert.equal(pluginSdkBuildStep(root).action, "skipped");
|
|
assert.equal(builds().length, 2);
|
|
|
|
writeFileSync(path.join(root, "packages/plugins/sdk/src/index.ts"), "export const changed = true;\n");
|
|
assert.equal(pluginSdkBuildStep(root).action, "built");
|
|
assert.deepEqual(builds().slice(2), ["packages/plugins/sdk"]);
|
|
});
|
|
|
|
test("a missing SDK dist is rebuilt even when the shared build is current", t => {
|
|
const { root, builds } = worktree(t);
|
|
pluginSdkBuildStep(root);
|
|
rmSync(path.join(root, "packages/plugins/sdk/dist"), { recursive: true, force: true });
|
|
assert.equal(pluginSdkBuildStep(root).action, "built");
|
|
assert.deepEqual(builds().slice(2), ["packages/plugins/sdk"]);
|
|
});
|
|
|
|
test("a failed build fails the preflight", t => {
|
|
const { root } = worktree(t);
|
|
writeFileSync(path.join(root, "fail"), "");
|
|
assert.throws(() => pluginSdkBuildStep(root), /ensure-plugin-build-deps\.mjs failed/);
|
|
});
|
|
|
|
test("a build that reports success without its outputs fails the preflight", t => {
|
|
const { root } = worktree(t);
|
|
assert.throws(() => pluginSdkBuildStep(root, () => 0), /no completion marker/);
|
|
});
|